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,462 @@
1
+ /**
2
+ * auggy create <name> — scaffold a new agent directory.
3
+ *
4
+ * Default location: ~/.auggy/agents/<name>/. Override with --dir <path>
5
+ * for git-tracked / project-folder layouts. Writes an entry to the
6
+ * agent index (~/.auggy/agents.json) on success.
7
+ *
8
+ * Refuses if:
9
+ * - CWD contains agent.yaml (operator likely meant `cd ..` first)
10
+ * - <name> already in the index
11
+ * - target dir already exists on disk
12
+ */
13
+
14
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
15
+ import { homedir } from "node:os";
16
+ import { join, resolve } from "node:path";
17
+ import { randomUUID } from "node:crypto";
18
+ import { checkbox, confirm, select, input } from "@inquirer/prompts";
19
+ import { stringify } from "yaml";
20
+ import { AUGMENT_CATALOG, type CatalogEntry } from "../augment-catalog";
21
+ import { copyBundledSkill, renderIdentityFromTemplate } from "../scaffold-skills";
22
+ import { addAgent, getAgent } from "../agent-index";
23
+ import { getModelChoices, formatChoiceLabel, type Provider } from "../model-picker";
24
+
25
+ const PROVIDER_DEFAULTS: Record<Provider, { model: string; envVar: string }> = {
26
+ anthropic: { model: "claude-sonnet-4-6", envVar: "ANTHROPIC_API_KEY" },
27
+ openai: { model: "gpt-5", envVar: "OPENAI_API_KEY" },
28
+ openrouter: {
29
+ model: "anthropic/claude-sonnet-4-6",
30
+ envVar: "OPENROUTER_API_KEY",
31
+ },
32
+ };
33
+
34
+ /**
35
+ * Default values surfaced when an operator skips the interactive prompts
36
+ * (Ctrl+D, non-TTY stdin, etc.). The operator-name default matches the
37
+ * security-eval test fixture's fallback so non-interactive scaffolding stays
38
+ * compatible with the canonical eval suite (see evals/security/eval-context.ts
39
+ * deriveOperatorName fallback).
40
+ */
41
+ const DEFAULT_OPERATOR_NAME = "the operator";
42
+ const DEFAULT_PURPOSE = "a helpful assistant";
43
+ const DEFAULT_ORG_NAME = "Test Org";
44
+ const DEFAULT_ORG_PURPOSE = "for testing only";
45
+
46
+ // ANSI color helpers. Truecolor #FBF7EB ("cream") matches the facility palette.
47
+ // Strips to plain text when stdout is not a TTY so piped output stays clean.
48
+ const IS_TTY = Boolean(process.stdout.isTTY);
49
+ const ansi = (code: string, s: string): string => (IS_TTY ? `\x1b[${code}m${s}\x1b[0m` : s);
50
+ const bold = (s: string): string => ansi("1", s);
51
+ const dim = (s: string): string => ansi("2", s);
52
+ const cream = (s: string): string => ansi("38;2;251;247;235", s);
53
+ const green = (s: string): string => ansi("32", s);
54
+
55
+ export async function runCreate(name: string, opts: { dir?: string }): Promise<void> {
56
+ // Wrong-dir guard: refuse if CWD has agent.yaml. Skip when --dir is provided
57
+ // (operator has explicitly chosen the target, so CWD is irrelevant).
58
+ if (!opts.dir) {
59
+ const cwdAgentYaml = resolve("./agent.yaml");
60
+ if (existsSync(cwdAgentYaml)) {
61
+ throw new Error(
62
+ `You appear to be inside an agent directory.\n\n` +
63
+ ` Found: ${cwdAgentYaml}\n\n` +
64
+ `Run \`cd ..\` first, or pass --dir <path> to scaffold elsewhere.`,
65
+ );
66
+ }
67
+ }
68
+
69
+ // Refuse if name already registered in the index.
70
+ const existing = getAgent(name);
71
+ if (existing) {
72
+ throw new Error(
73
+ `Agent "${name}" already exists at ${existing.localDir}.\n\n` +
74
+ ` Use a different name, or remove the existing one with \`auggy remove ${name}\`.`,
75
+ );
76
+ }
77
+
78
+ // Resolve target directory.
79
+ const dir = opts.dir ? resolve(opts.dir) : join(homedir(), ".auggy", "agents", name);
80
+
81
+ if (existsSync(dir)) {
82
+ throw new Error(`Directory already exists: ${dir}`);
83
+ }
84
+
85
+ printWelcome();
86
+
87
+ // Interactive engine selection.
88
+ const provider = await select<Provider>({
89
+ message: "Engine provider:",
90
+ choices: [
91
+ { name: "anthropic — Claude models", value: "anthropic" },
92
+ { name: "openai — GPT models", value: "openai" },
93
+ { name: "openrouter — any model via OpenRouter", value: "openrouter" },
94
+ ],
95
+ default: "anthropic",
96
+ });
97
+
98
+ // Model selection: dropdown of priced models + Custom escape hatch.
99
+ const CUSTOM_SENTINEL = "__custom__";
100
+ const choices = getModelChoices(provider);
101
+ const modelSelection = await select<string>({
102
+ message: "Model:",
103
+ choices: [
104
+ ...choices.map((c) => ({ name: formatChoiceLabel(c), value: c.id })),
105
+ { name: "Custom — type your own model ID", value: CUSTOM_SENTINEL },
106
+ ],
107
+ });
108
+
109
+ let model: string;
110
+ if (modelSelection === CUSTOM_SENTINEL) {
111
+ model = await input({ message: "Custom model ID:" });
112
+ printCustomModelWarning(model);
113
+ const proceed = await confirm({
114
+ message: "Continue with unpriced model? Budget caps will not enforce.",
115
+ default: false,
116
+ });
117
+ if (!proceed) {
118
+ throw new Error(
119
+ "Aborted by operator. Pick a priced model or add `engine.costOverride` to agent.yaml after scaffolding.",
120
+ );
121
+ }
122
+ } else {
123
+ model = modelSelection;
124
+ }
125
+
126
+ // Operator + purpose prompts. Used to populate identity.md security rules
127
+ // (operator-name reference) and the agent.yaml `operators[]` array.
128
+ const operatorName = await input({
129
+ message: "Operator name (your name; appears in identity.md security rule):",
130
+ default: DEFAULT_OPERATOR_NAME,
131
+ });
132
+ const purpose = await input({
133
+ message: "Agent purpose (one sentence):",
134
+ default: DEFAULT_PURPOSE,
135
+ });
136
+
137
+ // Interactive augment selection.
138
+ const selected = await checkbox({
139
+ message: "Select augments:",
140
+ choices: AUGMENT_CATALOG.map((entry) => ({
141
+ name: `${entry.label} — ${entry.description}`,
142
+ value: entry,
143
+ checked: entry.required,
144
+ disabled: entry.required ? "(always included)" : false,
145
+ })),
146
+ });
147
+
148
+ // Ensure required augments are included.
149
+ const augments = AUGMENT_CATALOG.filter((e) => e.required);
150
+ for (const entry of selected) {
151
+ if (!augments.includes(entry)) {
152
+ augments.push(entry);
153
+ }
154
+ }
155
+
156
+ // Conditional org prompts — only ask when orgContext is selected.
157
+ const orgContextSelected = augments.some((e) => e.type === "orgContext");
158
+ let orgName = DEFAULT_ORG_NAME;
159
+ let orgPurpose = DEFAULT_ORG_PURPOSE;
160
+ if (orgContextSelected) {
161
+ orgName = await input({
162
+ message: "Org name:",
163
+ default: DEFAULT_ORG_NAME,
164
+ });
165
+ orgPurpose = await input({
166
+ message: "Org purpose (one sentence):",
167
+ default: DEFAULT_ORG_PURPOSE,
168
+ });
169
+ }
170
+
171
+ const id = `aug1_${randomUUID()}`;
172
+
173
+ // Scaffold the directory.
174
+ let scaffoldComplete = false;
175
+ try {
176
+ mkdirSync(dir, { recursive: true });
177
+ mkdirSync(join(dir, "skills"), { recursive: true });
178
+ mkdirSync(join(dir, "workspace"), { recursive: true });
179
+ mkdirSync(join(dir, "augments"), { recursive: true });
180
+
181
+ console.log();
182
+ console.log(dim(" Installing augments..."));
183
+ console.log();
184
+ for (const entry of augments) {
185
+ // Copy bundled skill folder if the augment has one — overrides any
186
+ // legacy inline skillTemplate from the catalog. Idempotent.
187
+ copyBundledSkill(entry.type, dir);
188
+ console.log(` ${green("✓")} ${cream(entry.defaultName)} ${dim(`(${entry.type})`)}`);
189
+ }
190
+
191
+ const config = buildAgentYaml(id, name, augments, {
192
+ provider,
193
+ model,
194
+ operatorName,
195
+ purpose,
196
+ });
197
+ writeFileSync(join(dir, "agent.yaml"), config);
198
+
199
+ // Per ADR-030: identity.md no longer carries a skill manifest. The
200
+ // runtime `skills` augment surfaces the listing from disk.
201
+ writeFileSync(
202
+ join(dir, "identity.md"),
203
+ renderIdentityFromTemplate({
204
+ agentName: name,
205
+ purpose,
206
+ operatorName,
207
+ }),
208
+ );
209
+
210
+ if (augments.some((e) => e.defaultName === "learned")) {
211
+ writeFileSync(join(dir, "learned.md"), "");
212
+ }
213
+
214
+ // Scaffold an example org-context/ directory when orgContext is selected
215
+ // (per spec §Decision 9). The orgContext augment's file:// scheme support
216
+ // (α-6) lets this work without standing up an HTTP server.
217
+ if (orgContextSelected) {
218
+ writeOrgContextExample(dir, { orgName, orgPurpose, operatorName });
219
+ }
220
+
221
+ const envVars = collectEnvVars(augments, provider);
222
+ writeFileSync(join(dir, ".env.example"), buildEnvExample(envVars));
223
+ writeFileSync(join(dir, ".gitignore"), GITIGNORE);
224
+
225
+ scaffoldComplete = true;
226
+ } finally {
227
+ // Best-effort cleanup if scaffolding partially failed.
228
+ if (!scaffoldComplete && existsSync(dir)) {
229
+ try {
230
+ rmSync(dir, { recursive: true, force: true });
231
+ } catch {
232
+ // best-effort
233
+ }
234
+ }
235
+ }
236
+
237
+ // SIGINT handler scoped to the post-scaffold/pre-index window.
238
+ // Without this, Ctrl+C between scaffold completion and addAgent leaves
239
+ // an orphan dir with no index entry — neither create nor remove can recover.
240
+ const sigintHandler = (): void => {
241
+ if (existsSync(dir)) {
242
+ try {
243
+ rmSync(dir, { recursive: true, force: true });
244
+ } catch {
245
+ // best-effort
246
+ }
247
+ }
248
+ console.log();
249
+ console.log("Aborted. Cleaned up partially-scaffolded directory.");
250
+ process.exit(130); // 128 + SIGINT(2)
251
+ };
252
+ process.once("SIGINT", sigintHandler);
253
+
254
+ // Register in the index. If this fails, clean up the scaffolded dir.
255
+ try {
256
+ addAgent(name, dir);
257
+ } catch (err) {
258
+ process.removeListener("SIGINT", sigintHandler);
259
+ try {
260
+ rmSync(dir, { recursive: true, force: true });
261
+ } catch {
262
+ // best-effort
263
+ }
264
+ throw err;
265
+ }
266
+ process.removeListener("SIGINT", sigintHandler);
267
+
268
+ const envVar = PROVIDER_DEFAULTS[provider].envVar;
269
+
270
+ console.log();
271
+ console.log(dim(" ─────────────────────────────────────────────"));
272
+ console.log();
273
+ console.log(` ${green("✓")} ${bold(cream(`Agent "${name}" created`))}`);
274
+ console.log(` ${dim(dir)}`);
275
+ console.log();
276
+ console.log(` ${bold("Next steps:")}`);
277
+ console.log();
278
+ console.log(` ${cream("1.")} cp ${dir}/.env.example ${dir}/.env`);
279
+ console.log(` ${cream("2.")} Add your ${bold(envVar)} to ${dir}/.env`);
280
+ console.log(` ${cream("3.")} Edit ${dir}/identity.md`);
281
+ console.log(` ${cream("4.")} auggy dev ${name}`);
282
+ console.log();
283
+ }
284
+
285
+ function printCustomModelWarning(modelId: string): void {
286
+ console.log();
287
+ console.log(`⚠ Warning: "${modelId}" is not in the pricing table.`);
288
+ console.log();
289
+ console.log(` - Budgets augment cannot enforce dailyBudgetUsd or maxUsdPerDay for this model.`);
290
+ console.log(` - Eval cost-per-task tracking will report unpriced.`);
291
+ console.log(` - Future facility cost rollups will not include this agent.`);
292
+ console.log();
293
+ console.log(`Restore cost tracking by adding engine.costOverride to agent.yaml:`);
294
+ console.log();
295
+ console.log(` engine:`);
296
+ console.log(` costOverride:`);
297
+ console.log(` inputUsdPerMtok: <number>`);
298
+ console.log(` outputUsdPerMtok: <number>`);
299
+ console.log();
300
+ console.log(`Confirm at the next prompt to proceed, or decline to abort.`);
301
+ console.log();
302
+ }
303
+
304
+ // ---------------------------------------------------------------------------
305
+ // Helpers
306
+ // ---------------------------------------------------------------------------
307
+
308
+ function printWelcome(): void {
309
+ const banner = [
310
+ " █████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ██╗",
311
+ " ██╔══██╗██║ ██║██╔════╝ ██╔════╝ ╚██╗ ██╔╝",
312
+ " ███████║██║ ██║██║ ███╗██║ ███╗ ╚████╔╝",
313
+ " ██╔══██║██║ ██║██║ ██║██║ ██║ ╚██╔╝",
314
+ " ██║ ██║╚██████╔╝╚██████╔╝╚██████╔╝ ██║",
315
+ " ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝",
316
+ ];
317
+
318
+ console.log();
319
+ for (const line of banner) console.log(cream(line));
320
+ console.log();
321
+ console.log(` ${bold("augment-1")} ${dim("· by the Loosely Organized Research Facility")}`);
322
+ console.log();
323
+ console.log(" Auggy is a modular agent runtime. Agents are composed from");
324
+ console.log(" swappable augments — the kernel manages context, tools,");
325
+ console.log(" permissions, and lifecycle.");
326
+ console.log();
327
+ console.log(dim(" ─────────────────────────────────────────────"));
328
+ console.log();
329
+ console.log(" Let's configure your agent. Start by picking an engine.");
330
+ console.log();
331
+ console.log(dim(" The engine is the LLM provider the kernel calls each turn —"));
332
+ console.log(dim(" one per agent (Anthropic, OpenAI, OpenRouter). Augments plug in"));
333
+ console.log(dim(" around it. Both are swappable later in agent.yaml."));
334
+ console.log();
335
+ }
336
+
337
+ function buildAgentYaml(
338
+ id: string,
339
+ name: string,
340
+ augments: CatalogEntry[],
341
+ engine: { provider: Provider; model: string; operatorName: string; purpose: string },
342
+ ): string {
343
+ const config: Record<string, unknown> = {
344
+ id,
345
+ name,
346
+ purpose: engine.purpose,
347
+ operators: [engine.operatorName],
348
+ // identity shorthand — synthesizes the fileMemory@system entry at parse
349
+ // time (per α-5). Operators wanting non-default options should drop the
350
+ // shorthand and add an explicit fileMemory augment instead.
351
+ identity: "./identity.md",
352
+ engine: {
353
+ provider: engine.provider,
354
+ model: engine.model,
355
+ maxContextTokens: 200000,
356
+ maxTokens: 4096,
357
+ },
358
+ settings: {
359
+ compactionStrategy: "truncate",
360
+ maxInferenceLoops: 10,
361
+ },
362
+ augments: augments.map((entry) => {
363
+ const options = layeredMemoryNamespaceFor(entry, name) ?? entry.defaultOptions;
364
+ return {
365
+ name: entry.defaultName,
366
+ type: entry.type,
367
+ options,
368
+ };
369
+ }),
370
+ };
371
+
372
+ return `# Agent configuration\n\n${stringify(config)}`;
373
+ }
374
+
375
+ /**
376
+ * For a layeredMemory catalog entry, substitute the agent name for the
377
+ * placeholder namespace so each scaffolded agent gets its own logical
378
+ * storage namespace out of the box. Returns `null` for non-layeredMemory
379
+ * entries so the caller can fall back to defaults.
380
+ */
381
+ function layeredMemoryNamespaceFor(
382
+ entry: CatalogEntry,
383
+ agentName: string,
384
+ ): Record<string, unknown> | null {
385
+ if (entry.type !== "layeredMemory") return null;
386
+ return { ...entry.defaultOptions, namespace: agentName };
387
+ }
388
+
389
+ function collectEnvVars(augments: CatalogEntry[], provider: Provider): string[] {
390
+ const vars = new Set<string>([PROVIDER_DEFAULTS[provider].envVar]);
391
+ for (const entry of augments) {
392
+ if (entry.envVars) {
393
+ for (const v of entry.envVars) vars.add(v);
394
+ }
395
+ }
396
+ return [...vars];
397
+ }
398
+
399
+ function buildEnvExample(vars: string[]): string {
400
+ const lines = ["# Agent secrets — copy to .env and fill in values.", "# .env is gitignored.", ""];
401
+ for (const v of vars) {
402
+ lines.push(`${v}=`);
403
+ }
404
+ return `${lines.join("\n")}\n`;
405
+ }
406
+
407
+ /**
408
+ * Write a minimal example `org-context/` directory the orgContext augment
409
+ * can read with `baseUrl: file://./org-context` (α-6). Lets the operator
410
+ * see the augment work end-to-end without standing up an HTTP service.
411
+ */
412
+ function writeOrgContextExample(
413
+ agentDir: string,
414
+ values: { orgName: string; orgPurpose: string; operatorName: string },
415
+ ): void {
416
+ const orgDir = join(agentDir, "org-context");
417
+ mkdirSync(orgDir, { recursive: true });
418
+
419
+ const manifest = {
420
+ org: values.orgName,
421
+ purpose: values.orgPurpose,
422
+ operator: values.operatorName,
423
+ phase: "active",
424
+ endpoints: [
425
+ { path: "/mission", description: "Org mission and active focus" },
426
+ { path: "/team", description: "People and roles" },
427
+ ],
428
+ };
429
+ writeFileSync(join(orgDir, "manifest"), `${JSON.stringify(manifest, null, 2)}\n`);
430
+ writeFileSync(
431
+ join(orgDir, "mission.md"),
432
+ `# ${values.orgName} — Mission\n\n${values.orgPurpose}\n`,
433
+ );
434
+ writeFileSync(
435
+ join(orgDir, "team.md"),
436
+ `# ${values.orgName} — Team\n\n- ${values.operatorName} (operator)\n`,
437
+ );
438
+ writeFileSync(
439
+ join(orgDir, "README.md"),
440
+ `# Org context (example)\n\nThis directory backs the orgContext augment via the\n\`baseUrl: file://./org-context\` config in agent.yaml.\n\n- \`manifest\` — JSON listing endpoints the augment exposes\n- \`mission.md\`, \`team.md\` — endpoint targets the manifest references\n\nReplace these files with your real org content, or change \`baseUrl\` in\n\`agent.yaml\` to point at an HTTP-served manifest if you'd rather host\nyour org context elsewhere.\n`,
441
+ );
442
+ }
443
+
444
+ const GITIGNORE = `.env
445
+ .env.local
446
+ workspace/
447
+ *.log
448
+ *.err
449
+ node_modules/
450
+ memory.sqlite
451
+ memory.sqlite-journal
452
+ memory.sqlite-wal
453
+ memory.sqlite-shm
454
+ memory.db
455
+ memory.db-journal
456
+ memory.db-wal
457
+ memory.db-shm
458
+ budgets.db
459
+ budgets.db-journal
460
+ budgets.db-wal
461
+ budgets.db-shm
462
+ `;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * auggy dev <name> — run an agent in the foreground.
3
+ *
4
+ * This is the core lifecycle driver. Both `auggy dev` (interactive)
5
+ * and `auggy start` (via launchd) ultimately run this code path.
6
+ *
7
+ * Flow:
8
+ * 1. Parse config → ParsedConfig
9
+ * 2. Resolve engine → ModelClient
10
+ * 3. Resolve augments → Augment[]
11
+ * 4. defineAgent(config, model)
12
+ * 5. Write PID manifest
13
+ * 6. Register signal handlers
14
+ * 7. agent.start()
15
+ * 8. Wait for signal → agent.stop() → cleanup
16
+ */
17
+
18
+ import { resolve, dirname } from "node:path";
19
+ import { defineAgent } from "../../agent";
20
+ import { parseConfig } from "../config-parser";
21
+ import { resolveEngine } from "../engine-resolver";
22
+ import { resolveAugments } from "../augment-resolver";
23
+ import { writePidManifest, removePidManifest } from "../pid-registry";
24
+ import { resolveConfigPath } from "../resolve-config";
25
+ import type { AgentConfig, Augment, ModelClient } from "../../types";
26
+
27
+ /**
28
+ * Extract the webTransport port from augment configs (for the PID manifest).
29
+ */
30
+ function extractPort(config: ReturnType<typeof parseConfig>): number | null {
31
+ for (const aug of config.augments) {
32
+ if (aug.type === "webTransport" && aug.options?.port) {
33
+ return aug.options.port as number;
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ export async function runDev(
40
+ name: string,
41
+ opts: { config?: string; internalMode?: string },
42
+ ): Promise<void> {
43
+ const configPath = resolveConfigPath(name, opts.config);
44
+ const agentDir = dirname(configPath);
45
+ const mode = opts.internalMode === "launchd" ? ("launchd" as const) : ("dev" as const);
46
+
47
+ // Parse and validate config.
48
+ const config = parseConfig(configPath);
49
+
50
+ if (config.name !== name) {
51
+ console.warn(
52
+ `Warning: agent name in config ("${config.name}") differs from CLI argument ("${name}"). Using "${config.name}".`,
53
+ );
54
+ }
55
+
56
+ const agentName = config.name;
57
+
58
+ // Claim the name by writing the PID manifest atomically (wx flag).
59
+ // This prevents TOCTOU races — the filesystem is the lock.
60
+ const port = extractPort(config);
61
+ try {
62
+ writePidManifest({
63
+ pid: process.pid,
64
+ name: agentName,
65
+ port,
66
+ configPath: resolve(configPath),
67
+ agentDir: resolve(agentDir),
68
+ startedAt: new Date().toISOString(),
69
+ mode,
70
+ });
71
+ } catch (err) {
72
+ if ((err as NodeJS.ErrnoException).code === "EEXIST") {
73
+ throw new Error(
74
+ `Agent "${agentName}" is already running. Use "auggy stop ${agentName}" first.`,
75
+ );
76
+ }
77
+ throw err;
78
+ }
79
+
80
+ // From here on, clean up the PID manifest on any failure.
81
+ let model: ModelClient;
82
+ let augments: Augment[];
83
+ let agentConfig: AgentConfig;
84
+ try {
85
+ model = resolveEngine(config.engine);
86
+ augments = await resolveAugments(config.augments, agentDir);
87
+ agentConfig = {
88
+ name: agentName,
89
+ purpose: config.purpose,
90
+ model: config.engine.model,
91
+ augments,
92
+ operators: config.operators,
93
+ contextBudget: config.settings.contextBudget,
94
+ compactionStrategy: config.settings.compactionStrategy,
95
+ maxInferenceLoops: config.settings.maxInferenceLoops,
96
+ };
97
+ } catch (err) {
98
+ removePidManifest(agentName);
99
+ throw err;
100
+ }
101
+
102
+ // Create and start the agent.
103
+ const agent = defineAgent(agentConfig, model);
104
+
105
+ // Graceful shutdown handler.
106
+ let stopping = false;
107
+ const shutdown = async (signal: string) => {
108
+ if (stopping) return;
109
+ stopping = true;
110
+ console.log(`\n${signal} received — shutting down ${agentName}...`);
111
+ try {
112
+ await agent.stop();
113
+ } catch (err) {
114
+ console.error("Error during shutdown:", err);
115
+ }
116
+ removePidManifest(agentName);
117
+ console.log(`${agentName} stopped.`);
118
+ process.exit(0);
119
+ };
120
+
121
+ process.on("SIGINT", () => void shutdown("SIGINT"));
122
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
123
+
124
+ // Also clean up PID on unexpected exit.
125
+ process.on("exit", () => {
126
+ removePidManifest(agentName);
127
+ });
128
+
129
+ // Start the agent.
130
+ await agent.start();
131
+
132
+ console.log(`Agent "${agentName}" running (PID ${process.pid})`);
133
+ if (port) {
134
+ console.log(` Transport: http://localhost:${port}`);
135
+ console.log(` Health: http://localhost:${port}/health`);
136
+ }
137
+ console.log(` Config: ${configPath}`);
138
+ console.log(` Press Ctrl-C to stop.`);
139
+ }