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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/README.md +161 -0
- package/package.json +76 -0
- package/src/agent-card.ts +39 -0
- package/src/agent.ts +283 -0
- package/src/agentmail-client.ts +138 -0
- package/src/augments/bash/index.ts +463 -0
- package/src/augments/bash/skill/SKILL.md +156 -0
- package/src/augments/budgets/budget-store.ts +513 -0
- package/src/augments/budgets/index.ts +134 -0
- package/src/augments/budgets/preamble.ts +93 -0
- package/src/augments/budgets/types.ts +89 -0
- package/src/augments/file-memory/index.ts +71 -0
- package/src/augments/filesystem/index.ts +533 -0
- package/src/augments/filesystem/skill/SKILL.md +142 -0
- package/src/augments/filesystem/skill/references/mount-permissions.md +81 -0
- package/src/augments/layered-memory/extractor/buffer.ts +56 -0
- package/src/augments/layered-memory/extractor/frequency.ts +79 -0
- package/src/augments/layered-memory/extractor/inject-handler.ts +103 -0
- package/src/augments/layered-memory/extractor/parse.ts +75 -0
- package/src/augments/layered-memory/extractor/prompt.md +26 -0
- package/src/augments/layered-memory/index.ts +757 -0
- package/src/augments/layered-memory/skill/SKILL.md +153 -0
- package/src/augments/layered-memory/storage/migrations/README.md +16 -0
- package/src/augments/layered-memory/storage/migrations/supabase-add-fact-fields.sql +9 -0
- package/src/augments/layered-memory/storage/sqlite-store.ts +352 -0
- package/src/augments/layered-memory/storage/supabase-store.ts +263 -0
- package/src/augments/layered-memory/storage/types.ts +98 -0
- package/src/augments/link/index.ts +489 -0
- package/src/augments/link/translate.ts +261 -0
- package/src/augments/notify/adapters/agentmail.ts +70 -0
- package/src/augments/notify/adapters/telegram.ts +60 -0
- package/src/augments/notify/adapters/webhook.ts +55 -0
- package/src/augments/notify/index.ts +284 -0
- package/src/augments/notify/skill/SKILL.md +150 -0
- package/src/augments/org-context/index.ts +721 -0
- package/src/augments/org-context/skill/SKILL.md +96 -0
- package/src/augments/skills/index.ts +103 -0
- package/src/augments/supabase-memory/index.ts +151 -0
- package/src/augments/telegram-transport/index.ts +312 -0
- package/src/augments/telegram-transport/polling.ts +55 -0
- package/src/augments/telegram-transport/webhook.ts +56 -0
- package/src/augments/turn-control/index.ts +61 -0
- package/src/augments/turn-control/skill/SKILL.md +155 -0
- package/src/augments/visitor-auth/email-validation.ts +66 -0
- package/src/augments/visitor-auth/index.ts +779 -0
- package/src/augments/visitor-auth/rate-limiter.ts +90 -0
- package/src/augments/visitor-auth/skill/SKILL.md +55 -0
- package/src/augments/visitor-auth/storage/sqlite-store.ts +398 -0
- package/src/augments/visitor-auth/storage/types.ts +164 -0
- package/src/augments/visitor-auth/types.ts +123 -0
- package/src/augments/visitor-auth/verify-page.ts +179 -0
- package/src/augments/web-fetch/index.ts +331 -0
- package/src/augments/web-fetch/skill/SKILL.md +100 -0
- package/src/cli/agent-index.ts +289 -0
- package/src/cli/augment-catalog.ts +320 -0
- package/src/cli/augment-resolver.ts +597 -0
- package/src/cli/commands/add-skill.ts +194 -0
- package/src/cli/commands/add.ts +87 -0
- package/src/cli/commands/chat.ts +207 -0
- package/src/cli/commands/create.ts +462 -0
- package/src/cli/commands/dev.ts +139 -0
- package/src/cli/commands/eval.ts +180 -0
- package/src/cli/commands/ls.ts +66 -0
- package/src/cli/commands/remove.ts +95 -0
- package/src/cli/commands/restart.ts +40 -0
- package/src/cli/commands/start.ts +123 -0
- package/src/cli/commands/status.ts +104 -0
- package/src/cli/commands/stop.ts +84 -0
- package/src/cli/commands/visitors-revoke.ts +155 -0
- package/src/cli/commands/visitors.ts +101 -0
- package/src/cli/config-parser.ts +1034 -0
- package/src/cli/engine-resolver.ts +68 -0
- package/src/cli/index.ts +178 -0
- package/src/cli/model-picker.ts +89 -0
- package/src/cli/pid-registry.ts +146 -0
- package/src/cli/plist-generator.ts +117 -0
- package/src/cli/resolve-config.ts +56 -0
- package/src/cli/scaffold-skills.ts +158 -0
- package/src/cli/scaffold.ts +291 -0
- package/src/cli/skill-frontmatter.ts +51 -0
- package/src/cli/skill-validator.ts +151 -0
- package/src/cli/types.ts +228 -0
- package/src/cli/yaml-helpers.ts +66 -0
- package/src/engines/_shared/cost.ts +55 -0
- package/src/engines/_shared/schema-normalize.ts +75 -0
- package/src/engines/anthropic/pricing.ts +117 -0
- package/src/engines/anthropic.ts +483 -0
- package/src/engines/openai/pricing.ts +67 -0
- package/src/engines/openai.ts +446 -0
- package/src/engines/openrouter/pricing.ts +83 -0
- package/src/engines/openrouter.ts +185 -0
- package/src/helpers.ts +24 -0
- package/src/http.ts +387 -0
- package/src/index.ts +165 -0
- package/src/kernel/capability-table.ts +172 -0
- package/src/kernel/context-allocator.ts +161 -0
- package/src/kernel/history-manager.ts +198 -0
- package/src/kernel/lifecycle-manager.ts +106 -0
- package/src/kernel/output-validator.ts +35 -0
- package/src/kernel/preamble.ts +23 -0
- package/src/kernel/route-collector.ts +97 -0
- package/src/kernel/timeout.ts +21 -0
- package/src/kernel/tool-selector.ts +47 -0
- package/src/kernel/trace-emitter.ts +66 -0
- package/src/kernel/transport-queue.ts +147 -0
- package/src/kernel/turn-loop.ts +1148 -0
- package/src/memory/context-synthesis.ts +83 -0
- package/src/memory/memory-bus.ts +61 -0
- package/src/memory/registry.ts +80 -0
- package/src/memory/tools.ts +320 -0
- package/src/memory/types.ts +8 -0
- package/src/parts.ts +30 -0
- package/src/scaffold-templates/identity.md +31 -0
- package/src/telegram-client.ts +145 -0
- package/src/tokenizer.ts +14 -0
- package/src/transports/ag-ui-events.ts +253 -0
- package/src/transports/visitor-token.ts +82 -0
- package/src/transports/web-transport.ts +948 -0
- package/src/types.ts +1009 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auggy add-skill <augment> — install a bundled augment skill into an agent.
|
|
3
|
+
*
|
|
4
|
+
* Companion command to the boot-time skill validator (PR α task 7) and the
|
|
5
|
+
* scaffold-time auto-copy in `auggy create` / `auggy add`. When an operator
|
|
6
|
+
* has an augment configured but no `skills/<augment>/SKILL.md` mounted (the
|
|
7
|
+
* augment was added before bundled-skill copying existed, an upgrade brought
|
|
8
|
+
* a new skill the operator wants, or the operator manually deleted the
|
|
9
|
+
* folder), this command copies `src/augments/<augment>/skill/*` into the
|
|
10
|
+
* agent directory at `<agent-dir>/skills/<augment>/`.
|
|
11
|
+
*
|
|
12
|
+
* The argument is the augment FOLDER NAME (kebab-case) — what the boot-time
|
|
13
|
+
* validator emits in its remediation hint and what shows up under
|
|
14
|
+
* `<agent-dir>/skills/`. Example: `auggy add-skill web-fetch`, NOT
|
|
15
|
+
* `auggy add-skill webFetch` (the camelCase YAML `type:` field).
|
|
16
|
+
*
|
|
17
|
+
* Per ADR-025 Decision 5 + spec Decision 7. Idempotent — re-running
|
|
18
|
+
* overwrites existing skill files (operator opt-in to updates).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { join, resolve } from "node:path";
|
|
23
|
+
import { Command } from "commander";
|
|
24
|
+
import { copyBundledSkill, augmentFolderForType } from "../scaffold-skills";
|
|
25
|
+
import { getAgent } from "../agent-index";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Inverse of `TYPE_TO_AUGMENT_FOLDER` exported via `augmentFolderForType`:
|
|
29
|
+
* we need the camelCase `type:` for `copyBundledSkill`, but the operator
|
|
30
|
+
* passes the folder name. The set of folder names is the canonical list of
|
|
31
|
+
* valid `<augment>` arguments. Built lazily so the data lives in one place
|
|
32
|
+
* (scaffold-skills.ts) and we don't drift.
|
|
33
|
+
*/
|
|
34
|
+
const KNOWN_TYPES = [
|
|
35
|
+
"filesystem",
|
|
36
|
+
"layeredMemory",
|
|
37
|
+
"webFetch",
|
|
38
|
+
"orgContext",
|
|
39
|
+
"bash",
|
|
40
|
+
"notify",
|
|
41
|
+
"turnControl",
|
|
42
|
+
"fileMemory",
|
|
43
|
+
"supabaseMemory",
|
|
44
|
+
"budgets",
|
|
45
|
+
"webTransport",
|
|
46
|
+
"telegramTransport",
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
function buildFolderToTypeMap(): Map<string, string> {
|
|
50
|
+
const map = new Map<string, string>();
|
|
51
|
+
for (const type of KNOWN_TYPES) {
|
|
52
|
+
const folder = augmentFolderForType(type);
|
|
53
|
+
if (folder) map.set(folder, type);
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the on-disk path to the bundled skill SKILL.md for a folder, used
|
|
60
|
+
* to verify the augment ships a skill at all before attempting to copy.
|
|
61
|
+
*/
|
|
62
|
+
function bundledSkillExists(folder: string): boolean {
|
|
63
|
+
const dir = resolve(import.meta.dir, "../../augments", folder, "skill", "SKILL.md");
|
|
64
|
+
return existsSync(dir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface ResolveAgentDirOptions {
|
|
68
|
+
/** Override `~/.auggy/` for tests. */
|
|
69
|
+
auggyDir?: string;
|
|
70
|
+
/** Override CWD for tests. Defaults to process.cwd(). */
|
|
71
|
+
cwd?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the agent directory from the optional --agent flag or CWD.
|
|
76
|
+
*
|
|
77
|
+
* - With `--agent <name>`: look up the registered agent in the index.
|
|
78
|
+
* - Without: use CWD; require an `agent.yaml` to be present so we don't
|
|
79
|
+
* silently create a `skills/` directory in the wrong place.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveAgentDir(
|
|
82
|
+
agentNameFlag: string | undefined,
|
|
83
|
+
opts: ResolveAgentDirOptions = {},
|
|
84
|
+
): string {
|
|
85
|
+
if (agentNameFlag) {
|
|
86
|
+
const entry = getAgent(agentNameFlag, { auggyDir: opts.auggyDir });
|
|
87
|
+
if (!entry) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Agent "${agentNameFlag}" is not registered.\n\n` +
|
|
90
|
+
` Run \`auggy ls\` to see registered agents.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
const cfg = join(entry.localDir, "agent.yaml");
|
|
94
|
+
if (!existsSync(cfg)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`agent.yaml missing at indexed path: ${cfg}\n\n` +
|
|
97
|
+
` The agent directory may have been deleted or moved manually.\n` +
|
|
98
|
+
` Run \`auggy remove ${agentNameFlag}\` to clean up the index entry.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return entry.localDir;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
105
|
+
const agentYaml = join(cwd, "agent.yaml");
|
|
106
|
+
if (!existsSync(agentYaml)) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Not an agent directory: no agent.yaml in ${cwd}.\n\n` +
|
|
109
|
+
` Run from inside an agent dir, or pass \`--agent <name>\` to target a registered agent.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return cwd;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface AddSkillCommandDeps {
|
|
116
|
+
/** Override exit so tests can assert the exit code without crashing the runner. */
|
|
117
|
+
exit?: (code: number) => void;
|
|
118
|
+
/** Override `~/.auggy/` for tests. */
|
|
119
|
+
auggyDir?: string;
|
|
120
|
+
/** Override CWD for tests. */
|
|
121
|
+
cwd?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function addSkillCommand(deps: AddSkillCommandDeps = {}): Command {
|
|
125
|
+
const exit = deps.exit ?? ((code: number) => process.exit(code));
|
|
126
|
+
|
|
127
|
+
return new Command("add-skill")
|
|
128
|
+
.description("Install a bundled augment skill into an existing agent")
|
|
129
|
+
.argument("<augment>", "augment folder name (kebab-case), e.g. web-fetch, layered-memory, bash")
|
|
130
|
+
.option("--agent <name>", "registered agent name (defaults to current directory)")
|
|
131
|
+
.addHelpText(
|
|
132
|
+
"after",
|
|
133
|
+
[
|
|
134
|
+
"",
|
|
135
|
+
"Examples:",
|
|
136
|
+
" cd my-agent && auggy add-skill web-fetch",
|
|
137
|
+
" auggy add-skill layered-memory --agent zip",
|
|
138
|
+
].join("\n"),
|
|
139
|
+
)
|
|
140
|
+
.action(async (augment: string, opts: { agent?: string }) => {
|
|
141
|
+
// 1. Resolve agent dir.
|
|
142
|
+
let agentDir: string;
|
|
143
|
+
try {
|
|
144
|
+
agentDir = resolveAgentDir(opts.agent, { auggyDir: deps.auggyDir, cwd: deps.cwd });
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error(`Error: ${(err as Error).message}`);
|
|
147
|
+
exit(1);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 2. Validate the augment folder name.
|
|
152
|
+
const folderToType = buildFolderToTypeMap();
|
|
153
|
+
const type = folderToType.get(augment);
|
|
154
|
+
if (!type) {
|
|
155
|
+
const valid = [...folderToType.keys()].sort().join(", ");
|
|
156
|
+
console.error(
|
|
157
|
+
`Error: Unknown augment "${augment}".\n\n` + ` Valid augment folder names: ${valid}`,
|
|
158
|
+
);
|
|
159
|
+
exit(1);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// 3. Verify the augment ships a bundled skill.
|
|
164
|
+
if (!bundledSkillExists(augment)) {
|
|
165
|
+
console.error(`Error: "${augment}" augment ships no bundled skill. Nothing to add-skill.`);
|
|
166
|
+
exit(1);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. Copy via the shared helper. Idempotent (overwrites existing files).
|
|
171
|
+
let copied: boolean;
|
|
172
|
+
try {
|
|
173
|
+
copied = copyBundledSkill(type, agentDir);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`Error: failed to copy skill files: ${(err as Error).message}`);
|
|
176
|
+
exit(2);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!copied) {
|
|
181
|
+
// Defensive — the bundledSkillExists check above should make this
|
|
182
|
+
// unreachable, but if copyBundledSkill returns false for any reason
|
|
183
|
+
// (e.g. mid-call filesystem disappearance) surface it as an error.
|
|
184
|
+
console.error(`Error: failed to copy bundled skill for "${augment}" (source not found).`);
|
|
185
|
+
exit(2);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
console.log(
|
|
190
|
+
`Installed bundled skill for "${augment}" -> ${join(agentDir, "skills", augment)}/`,
|
|
191
|
+
);
|
|
192
|
+
exit(0);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auggy add — add augments to an existing agent.
|
|
3
|
+
*
|
|
4
|
+
* Lists currently installed vs available augments. User selects from
|
|
5
|
+
* available. Updates agent.yaml and copies bundled
|
|
6
|
+
* `src/augments/<name>/skill/` folders into the agent dir. Per ADR-030
|
|
7
|
+
* the skill listing is owned by the runtime's 'skills' augment surface,
|
|
8
|
+
* NOT injected into identity.md — so no identity-file rewrite happens
|
|
9
|
+
* here. The model picks up new skills automatically because the 'skills'
|
|
10
|
+
* augment rescans its mounted dir at every context() call.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname } from "node:path";
|
|
15
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
16
|
+
import { checkbox } from "@inquirer/prompts";
|
|
17
|
+
import { getAvailableAugments, type CatalogEntry } from "../augment-catalog";
|
|
18
|
+
import { copyBundledSkill } from "../scaffold-skills";
|
|
19
|
+
import { resolveConfigPath } from "../resolve-config";
|
|
20
|
+
|
|
21
|
+
export async function runAdd(name: string, opts: { config?: string }): Promise<void> {
|
|
22
|
+
const configPath = resolveConfigPath(name, opts.config);
|
|
23
|
+
const agentDir = dirname(configPath);
|
|
24
|
+
|
|
25
|
+
// Parse current config.
|
|
26
|
+
const raw = parseYaml(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
|
27
|
+
const currentAugments = (raw.augments ?? []) as Array<{ type: string; name: string }>;
|
|
28
|
+
|
|
29
|
+
console.log(`Currently installed: ${currentAugments.map((a) => a.name).join(", ")}`);
|
|
30
|
+
|
|
31
|
+
// Find what's available to add.
|
|
32
|
+
const available = getAvailableAugments(currentAugments);
|
|
33
|
+
|
|
34
|
+
if (available.length === 0) {
|
|
35
|
+
console.log("All built-in augments are already installed.");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Interactive selection.
|
|
40
|
+
const selected = await checkbox<CatalogEntry>({
|
|
41
|
+
message: "Select augments to add:",
|
|
42
|
+
choices: available.map((entry) => ({
|
|
43
|
+
name: `${entry.label} — ${entry.description}`,
|
|
44
|
+
value: entry,
|
|
45
|
+
})),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (selected.length === 0) {
|
|
49
|
+
console.log("No augments selected.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Add to agent.yaml.
|
|
54
|
+
for (const entry of selected) {
|
|
55
|
+
currentAugments.push({
|
|
56
|
+
name: entry.defaultName,
|
|
57
|
+
type: entry.type,
|
|
58
|
+
...({ options: entry.defaultOptions } as Record<string, unknown>),
|
|
59
|
+
} as { type: string; name: string });
|
|
60
|
+
}
|
|
61
|
+
raw.augments = currentAugments;
|
|
62
|
+
|
|
63
|
+
writeFileSync(configPath, `# Agent configuration\n\n${stringifyYaml(raw)}`);
|
|
64
|
+
|
|
65
|
+
// Install skills — copy the bundled `src/augments/<name>/skill/` folder
|
|
66
|
+
// for each selected augment that ships one. Idempotent. Per ADR-030 the
|
|
67
|
+
// 'skills' augment surfaces them to the model automatically by rescanning
|
|
68
|
+
// the skills/ dir on every context() call; no identity.md edit needed.
|
|
69
|
+
console.log();
|
|
70
|
+
for (const entry of selected) {
|
|
71
|
+
copyBundledSkill(entry.type, agentDir);
|
|
72
|
+
console.log(` ✓ ${entry.defaultName} (${entry.type})`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Collect any new env vars needed.
|
|
76
|
+
const newEnvVars = selected.flatMap((e) => e.envVars ?? []);
|
|
77
|
+
if (newEnvVars.length > 0) {
|
|
78
|
+
console.log();
|
|
79
|
+
console.log("Add these to your .env:");
|
|
80
|
+
for (const v of newEnvVars) {
|
|
81
|
+
console.log(` ${v}=`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(`Restart to apply: auggy restart ${name}`);
|
|
87
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { existsSync, mkdirSync, createWriteStream, createReadStream } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve, dirname } from "node:path";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { pipeline } from "node:stream/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import packageJson from "../../../package.json" with { type: "json" };
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PORT = 8090;
|
|
12
|
+
const RELEASE_REPO = "looselyorganized/augment-1";
|
|
13
|
+
|
|
14
|
+
// Resolve the chat package directory relative to THIS module's location.
|
|
15
|
+
// Uses import.meta.url so it works under both source-tree Bun runs and
|
|
16
|
+
// future ESM-published builds.
|
|
17
|
+
//
|
|
18
|
+
// TODO(npm-packaging): when this CLI is published to npm, the chat/ package
|
|
19
|
+
// won't be a sibling of src/cli/commands/. Two paths:
|
|
20
|
+
// a) Bundle chat/dist/ as a static asset in the published CLI tarball
|
|
21
|
+
// and point at the asset path here.
|
|
22
|
+
// b) Publish @auggy/chat as a separate npm package and use
|
|
23
|
+
// `require.resolve("@auggy/chat/server")` (after also exposing server.js
|
|
24
|
+
// as a package "exports" entry).
|
|
25
|
+
// For now, the source-tree relative resolution is the only supported path.
|
|
26
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const CHAT_PACKAGE_DIR = resolve(MODULE_DIR, "../../../chat");
|
|
28
|
+
|
|
29
|
+
export function chatCommand(): Command {
|
|
30
|
+
const cmd = new Command("chat")
|
|
31
|
+
.description("Launch the Auggy operator chat surface (Local GUI)")
|
|
32
|
+
.option("-p, --port <port>", "GUI server port", String(DEFAULT_PORT))
|
|
33
|
+
.option("--no-open", "Don't auto-open the browser")
|
|
34
|
+
.option("--rebuild", "Rebuild the GUI dist from source (requires Bun + Vite in chat/)")
|
|
35
|
+
.action(async (opts: { port: string; open: boolean; rebuild: boolean }) => {
|
|
36
|
+
const port = Number(opts.port);
|
|
37
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
38
|
+
console.error(`Invalid --port value: ${opts.port}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const guiPackageDir = CHAT_PACKAGE_DIR;
|
|
43
|
+
const localDistDir = join(guiPackageDir, "dist");
|
|
44
|
+
const version = packageJson.version;
|
|
45
|
+
const cacheDistDir = join(homedir(), ".auggy", "chat", version, "dist");
|
|
46
|
+
|
|
47
|
+
// Lazy-load the chat server so a missing chat/ package (e.g. from an npm
|
|
48
|
+
// install that omits the chat/ directory) surfaces as a recoverable error
|
|
49
|
+
// instead of crashing the whole auggy CLI at module-load time.
|
|
50
|
+
let createGuiServer: typeof import("../../../chat/server").createGuiServer;
|
|
51
|
+
try {
|
|
52
|
+
({ createGuiServer } = await import("../../../chat/server"));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.error(
|
|
55
|
+
`[auggy chat] chat package not available: ${(err as Error).message}\n` +
|
|
56
|
+
`\n` +
|
|
57
|
+
`Recovery options:\n` +
|
|
58
|
+
` • If you are running from source: cd ${guiPackageDir} && bun install\n` +
|
|
59
|
+
` • If auggy was installed via npm and chat/ is missing, this is a\n` +
|
|
60
|
+
` packaging bug — please file an issue.`,
|
|
61
|
+
);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let distDir: string;
|
|
66
|
+
try {
|
|
67
|
+
if (opts.rebuild) {
|
|
68
|
+
console.log("[auggy chat] Rebuilding chat/dist via Vite...");
|
|
69
|
+
await runVite(guiPackageDir);
|
|
70
|
+
}
|
|
71
|
+
distDir = await resolveDistDir({
|
|
72
|
+
localDistDir,
|
|
73
|
+
cacheDistDir,
|
|
74
|
+
version,
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
console.error(
|
|
78
|
+
`[auggy chat] chat dist not found or failed to resolve: ${(err as Error).message}\n` +
|
|
79
|
+
`\n` +
|
|
80
|
+
`Recovery options:\n` +
|
|
81
|
+
` • If you are running from source: cd ${guiPackageDir} && bun install && bun run build\n` +
|
|
82
|
+
` • Or pass --rebuild to do that automatically: auggy chat --rebuild\n` +
|
|
83
|
+
` • If auggy was installed via npm and chat/ is missing, this is a\n` +
|
|
84
|
+
` packaging bug — please file an issue.`,
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let server: Awaited<ReturnType<typeof createGuiServer>>;
|
|
90
|
+
try {
|
|
91
|
+
server = createGuiServer({ port, staticDir: distDir });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(
|
|
94
|
+
`[auggy chat] Failed to start server on port ${port}: ${(err as Error).message}\n` +
|
|
95
|
+
`Try a different port: auggy chat --port ${port + 1}`,
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const url = `http://localhost:${port}`;
|
|
101
|
+
console.log(`[auggy chat] Local GUI ready at ${url}`);
|
|
102
|
+
console.log("[auggy chat] Ctrl-C to stop.");
|
|
103
|
+
|
|
104
|
+
if (opts.open) openBrowser(url);
|
|
105
|
+
|
|
106
|
+
const shutdown = (signal: string) => {
|
|
107
|
+
console.log(`\n[auggy chat] Received ${signal}, shutting down...`);
|
|
108
|
+
try {
|
|
109
|
+
server.stop();
|
|
110
|
+
} catch {
|
|
111
|
+
/* swallow */
|
|
112
|
+
}
|
|
113
|
+
process.exit(0);
|
|
114
|
+
};
|
|
115
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
116
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return cmd;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function resolveDistDir(opts: {
|
|
123
|
+
localDistDir: string;
|
|
124
|
+
cacheDistDir: string;
|
|
125
|
+
version: string;
|
|
126
|
+
}): Promise<string> {
|
|
127
|
+
if (existsSync(join(opts.localDistDir, "index.html"))) return opts.localDistDir;
|
|
128
|
+
if (existsSync(join(opts.cacheDistDir, "index.html"))) return opts.cacheDistDir;
|
|
129
|
+
|
|
130
|
+
console.log(
|
|
131
|
+
`[auggy chat] No cached dist for version ${opts.version}; downloading from GitHub release...`,
|
|
132
|
+
);
|
|
133
|
+
await downloadAndCache(opts.version, opts.cacheDistDir);
|
|
134
|
+
if (!existsSync(join(opts.cacheDistDir, "index.html"))) {
|
|
135
|
+
throw new Error("download succeeded but dist/index.html not found after extraction");
|
|
136
|
+
}
|
|
137
|
+
return opts.cacheDistDir;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function downloadAndCache(version: string, cacheDistDir: string): Promise<void> {
|
|
141
|
+
const tag = `v${version}`;
|
|
142
|
+
const tarballUrl = `https://github.com/${RELEASE_REPO}/releases/download/${tag}/chat-dist-${tag}.tar.gz`;
|
|
143
|
+
const checksumUrl = `${tarballUrl}.sha256`;
|
|
144
|
+
|
|
145
|
+
const cacheRoot = join(cacheDistDir, "..");
|
|
146
|
+
mkdirSync(cacheRoot, { recursive: true });
|
|
147
|
+
|
|
148
|
+
const tarballPath = join(cacheRoot, "dist.tar.gz");
|
|
149
|
+
const tarRes = await fetch(tarballUrl);
|
|
150
|
+
if (!tarRes.ok || !tarRes.body) {
|
|
151
|
+
throw new Error(`Download failed: ${tarRes.status} ${tarRes.statusText} from ${tarballUrl}`);
|
|
152
|
+
}
|
|
153
|
+
await pipeline(tarRes.body as unknown as NodeJS.ReadableStream, createWriteStream(tarballPath));
|
|
154
|
+
|
|
155
|
+
const checksumRes = await fetch(checksumUrl);
|
|
156
|
+
if (!checksumRes.ok) {
|
|
157
|
+
throw new Error(`Failed to fetch checksum: ${checksumRes.status} ${checksumRes.statusText}`);
|
|
158
|
+
}
|
|
159
|
+
const checksumLine = await checksumRes.text();
|
|
160
|
+
const expectedSha = checksumLine.split(/\s+/)[0];
|
|
161
|
+
const actualSha = await sha256File(tarballPath);
|
|
162
|
+
if (expectedSha !== actualSha) {
|
|
163
|
+
throw new Error(`Checksum mismatch: expected ${expectedSha}, got ${actualSha}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await runTar(tarballPath, cacheRoot);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sha256File(path: string): Promise<string> {
|
|
170
|
+
return new Promise((resolveP, rejectP) => {
|
|
171
|
+
const hash = createHash("sha256");
|
|
172
|
+
const stream = createReadStream(path);
|
|
173
|
+
stream.on("data", (chunk: Buffer | string) => hash.update(chunk));
|
|
174
|
+
stream.on("end", () => resolveP(hash.digest("hex")));
|
|
175
|
+
stream.on("error", rejectP);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function runTar(tarballPath: string, destDir: string): Promise<void> {
|
|
180
|
+
return new Promise((resolveP, rejectP) => {
|
|
181
|
+
const child = spawn("tar", ["-xzf", tarballPath, "-C", destDir], {
|
|
182
|
+
stdio: "inherit",
|
|
183
|
+
});
|
|
184
|
+
child.on("exit", (code) => (code === 0 ? resolveP() : rejectP(new Error(`tar exit ${code}`))));
|
|
185
|
+
child.on("error", rejectP);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function runVite(cwd: string): Promise<void> {
|
|
190
|
+
return new Promise((resolveP, rejectP) => {
|
|
191
|
+
const child = spawn("bun", ["run", "build"], { cwd, stdio: "inherit" });
|
|
192
|
+
child.on("exit", (code) =>
|
|
193
|
+
code === 0 ? resolveP() : rejectP(new Error(`Vite build exit ${code}`)),
|
|
194
|
+
);
|
|
195
|
+
child.on("error", rejectP);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function openBrowser(url: string): void {
|
|
200
|
+
const platform = process.platform;
|
|
201
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
202
|
+
try {
|
|
203
|
+
spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
|
|
204
|
+
} catch {
|
|
205
|
+
/* best-effort */
|
|
206
|
+
}
|
|
207
|
+
}
|