@azad-73/cli 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2026 et-azad. All rights reserved.
2
+
3
+ This software and its source code are proprietary and confidential.
4
+ No permission is granted to use, copy, modify, merge, publish, distribute,
5
+ sublicense, or sell any part of this software, except under a separate
6
+ written agreement with the copyright holder.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
9
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
10
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Azad73 (`azad`)
2
+
3
+ Agentic AI platform built on the [Pi](https://pi.dev) SDK — a branded terminal agent with a curated set of default agents. Local-first and BYO-key: your API key and sessions stay on your machine.
4
+
5
+ > "Azad" means *free / freedom* — local-first, BYO-key, no lock-in.
6
+
7
+ ## Install
8
+
9
+ Requires **Node ≥ 22.19**.
10
+
11
+ ```bash
12
+ npm i -g @azad-73/cli
13
+ ```
14
+
15
+ ## Authenticate
16
+
17
+ Launch the TUI and log in (recommended — supports API key or subscription/OAuth):
18
+
19
+ ```bash
20
+ azad
21
+ # then, inside the TUI:
22
+ /login
23
+ ```
24
+
25
+ Credentials are stored locally in `~/.azad/auth.json`. Alternatives:
26
+
27
+ ```bash
28
+ azad auth set # paste your Anthropic API key
29
+ # or use an environment variable:
30
+ export ANTHROPIC_API_KEY=sk-ant-...
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```bash
36
+ azad # launch the branded interactive TUI
37
+ azad agents list # list available agents
38
+ azad agents show <name> # show an agent (prompt + tools)
39
+ azad agents new <name> # scaffold a new agent in ~/.azad/agents
40
+ azad agents edit <name> # edit an agent ($EDITOR)
41
+ azad auth status|set|remove # manage the stored credential
42
+ azad version
43
+ ```
44
+
45
+ Default agents include `context-cartographer` (run first on a new repo — maps the
46
+ codebase and writes `AGENTS.md`), `feature-builder`, `feature-mr-reviewer`,
47
+ `bug-reproduction-helper`, `root-cause-investigator`, `bug-fixer`,
48
+ `bugfix-mr-reviewer`, and `ticket-doc-writer`.
49
+
50
+ ## Configuration
51
+
52
+ Everything lives under `~/.azad/`:
53
+
54
+ | Path | Contents |
55
+ |------|----------|
56
+ | `~/.azad/auth.json` | provider credentials (chmod 600) |
57
+ | `~/.azad/agents/` | your custom / overridden agents |
58
+ | `~/.azad/sessions/` | session history |
59
+
60
+ Select the bundled `azad73` theme in-TUI via `/settings`.
61
+
62
+ ## Notes
63
+
64
+ - Installing pulls one runtime dependency, the Pi SDK (`@earendil-works/pi-coding-agent`).
65
+ You may see `npm warn deprecated node-domexception@1.0.0` during install — it is a
66
+ harmless deprecation notice from a deep transitive dependency, not from Azad73.
67
+
68
+ ## License
69
+
70
+ Proprietary — all rights reserved. See [LICENSE](LICENSE).
package/dist/azad.js CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  // src/azad.ts
4
4
  import { spawn } from "node:child_process";
5
- import fs2 from "node:fs";
6
- import path4 from "node:path";
5
+ import fs4 from "node:fs";
6
+ import path6 from "node:path";
7
7
  import readline from "node:readline";
8
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
8
9
 
9
10
  // ../engine/src/index.ts
10
11
  import {
11
- SessionManager as SessionManager2,
12
- createAgentSession
12
+ SessionManager as SessionManager3,
13
+ createAgentSession as createAgentSession2
13
14
  } from "@earendil-works/pi-coding-agent";
14
15
 
15
16
  // ../shared/src/index.ts
@@ -84,52 +85,323 @@ import { InteractiveMode } from "@earendil-works/pi-coding-agent";
84
85
  // ../engine/src/runtime.ts
85
86
  import {
86
87
  SessionManager,
88
+ SettingsManager,
87
89
  createAgentSessionFromServices,
88
90
  createAgentSessionRuntime,
89
91
  createAgentSessionServices
90
92
  } from "@earendil-works/pi-coding-agent";
91
93
 
94
+ // ../engine/src/approval.ts
95
+ import "@earendil-works/pi-coding-agent";
96
+ var APPROVAL_MODES = [
97
+ { mode: "ask", short: "ask", label: "Ask \u2014 confirm before mutating tools" },
98
+ { mode: "auto", short: "auto-pilot", label: "Auto-pilot \u2014 run tools without confirmation" }
99
+ ];
100
+ var state = { mode: "ask", allow: /* @__PURE__ */ new Set() };
101
+ function getApprovalMode() {
102
+ return state.mode;
103
+ }
104
+ function approvalModeLabel(mode = state.mode) {
105
+ return APPROVAL_MODES.find((m) => m.mode === mode)?.short ?? mode;
106
+ }
107
+ var READ_ONLY = /* @__PURE__ */ new Set(["read", "grep", "find", "ls"]);
108
+ function truncate(s, max) {
109
+ const clean = s.replace(/\s+/g, " ").trim();
110
+ return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
111
+ }
112
+ function describe(toolName, input) {
113
+ if (toolName === "bash") return `bash: ${truncate(String(input.command ?? ""), 64)}`;
114
+ if (toolName === "write") return `write ${String(input.file_path ?? input.path ?? "")}`;
115
+ if (toolName === "edit") return `edit ${String(input.file_path ?? input.path ?? "")}`;
116
+ return toolName;
117
+ }
118
+ function bashProgram(command) {
119
+ const tokens = command.trim().split(/\s+/);
120
+ for (const tok of tokens) {
121
+ if (/^[A-Za-z_]\w*=/.test(tok)) continue;
122
+ return tok;
123
+ }
124
+ return tokens[0] ?? "";
125
+ }
126
+ function bashPrograms(command) {
127
+ if (/\$\(|`|<\(|>\(/.test(command)) return null;
128
+ const programs = /* @__PURE__ */ new Set();
129
+ for (const part of command.split(/&&|\|\||[|;\n]/)) {
130
+ const p = bashProgram(part);
131
+ if (p) programs.add(p);
132
+ }
133
+ return [...programs];
134
+ }
135
+ function createApprovalExtension() {
136
+ return (pi) => {
137
+ pi.on("tool_call", async (event, ctx) => {
138
+ if (state.mode === "auto") return void 0;
139
+ if (READ_ONLY.has(event.toolName)) return void 0;
140
+ const input = event.input;
141
+ const isBash = event.toolName === "bash";
142
+ const programs = isBash ? bashPrograms(String(input.command ?? "")) : null;
143
+ if (!isBash) {
144
+ if (state.allow.has(event.toolName)) return void 0;
145
+ } else if (programs && programs.length > 0 && programs.every((p) => state.allow.has(`bash:${p}`))) {
146
+ return void 0;
147
+ }
148
+ if (!ctx.hasUI) return { block: true, reason: `Approval required for ${event.toolName} (no UI to confirm)` };
149
+ const options = ["Yes", "No"];
150
+ let alwaysLabel;
151
+ let alwaysKeys = [];
152
+ if (!isBash) {
153
+ alwaysLabel = `Always allow ${event.toolName}`;
154
+ alwaysKeys = [event.toolName];
155
+ } else if (programs && programs.length > 0) {
156
+ alwaysKeys = programs.map((p) => `bash:${p}`);
157
+ alwaysLabel = programs.length === 1 ? `Always allow "${programs.join("")}" commands` : `Always allow: ${programs.join(", ")}`;
158
+ }
159
+ if (alwaysLabel) options.push(alwaysLabel);
160
+ const choice = await ctx.ui.select(`Approve ${describe(event.toolName, input)}`, options);
161
+ if (choice === void 0 || choice === "No") return { block: true, reason: "Denied by user" };
162
+ if (alwaysLabel && choice === alwaysLabel) for (const k of alwaysKeys) state.allow.add(k);
163
+ return void 0;
164
+ });
165
+ const setMode = (ctx, mode) => {
166
+ state.mode = mode;
167
+ ctx.ui.notify(`Approval mode: ${approvalModeLabel(mode)}`, mode === "auto" ? "warning" : "info");
168
+ };
169
+ const chooseMode = async (ctx) => {
170
+ const choice = await ctx.ui.select(
171
+ `Approval mode (current: ${approvalModeLabel()})`,
172
+ APPROVAL_MODES.map((m) => m.label)
173
+ );
174
+ const picked = APPROVAL_MODES.find((m) => m.label === choice);
175
+ if (picked) setMode(ctx, picked.mode);
176
+ };
177
+ pi.registerCommand("mode", { description: "Choose approval mode", handler: async (_args, ctx) => chooseMode(ctx) });
178
+ pi.registerCommand("ask", {
179
+ description: "Approval mode: ask before mutating tools",
180
+ handler: async (_args, ctx) => setMode(ctx, "ask")
181
+ });
182
+ pi.registerCommand("auto", {
183
+ description: "Approval mode: auto-pilot (run tools without confirmation)",
184
+ handler: async (_args, ctx) => setMode(ctx, "auto")
185
+ });
186
+ pi.registerShortcut("alt+a", { description: "Choose approval mode", handler: (ctx) => chooseMode(ctx) });
187
+ };
188
+ }
189
+
190
+ // ../engine/src/branding.ts
191
+ import os2 from "node:os";
192
+ import "@earendil-works/pi-coding-agent";
193
+
194
+ // ../engine/src/update.ts
195
+ import fs2 from "node:fs";
196
+ import path4 from "node:path";
197
+ var CLI_PACKAGE = "@azad-73/cli";
198
+ var CACHE_FILE = path4.join(AZAD_DIR, ".update-check.json");
199
+ var TTL_MS = 24 * 60 * 60 * 1e3;
200
+ async function getLatestVersion(timeoutMs = 3e3) {
201
+ try {
202
+ const controller = new AbortController();
203
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
204
+ const res = await fetch(`https://registry.npmjs.org/${CLI_PACKAGE}/latest`, { signal: controller.signal });
205
+ clearTimeout(timer);
206
+ if (!res.ok) return null;
207
+ const data = await res.json();
208
+ return data.version ?? null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function isNewerVersion(latest, current) {
214
+ const a = latest.split(".").map((n) => Number.parseInt(n, 10));
215
+ const b = current.split(".").map((n) => Number.parseInt(n, 10));
216
+ for (let i = 0; i < 3; i++) {
217
+ const x = a[i] ?? 0;
218
+ const y = b[i] ?? 0;
219
+ if (x !== y) return x > y;
220
+ }
221
+ return false;
222
+ }
223
+ function readCache() {
224
+ try {
225
+ return JSON.parse(fs2.readFileSync(CACHE_FILE, "utf8"));
226
+ } catch {
227
+ return null;
228
+ }
229
+ }
230
+ function writeCache(cache) {
231
+ try {
232
+ fs2.mkdirSync(AZAD_DIR, { recursive: true });
233
+ fs2.writeFileSync(CACHE_FILE, JSON.stringify(cache));
234
+ } catch {
235
+ }
236
+ }
237
+ async function checkForUpdate(current) {
238
+ const cache = readCache();
239
+ let latest;
240
+ if (cache && Date.now() - cache.checkedAt < TTL_MS) {
241
+ latest = cache.latest;
242
+ } else {
243
+ latest = await getLatestVersion();
244
+ writeCache({ checkedAt: Date.now(), latest });
245
+ }
246
+ return latest && isNewerVersion(latest, current) ? latest : null;
247
+ }
248
+ function cachedUpdate(current) {
249
+ const latest = readCache()?.latest;
250
+ return latest && isNewerVersion(latest, current) ? latest : null;
251
+ }
252
+
92
253
  // ../engine/src/branding.ts
93
- import { VERSION } from "@earendil-works/pi-coding-agent";
94
254
  function coreThemesDir() {
95
255
  return resolveCoreDataDir("themes", import.meta.url);
96
256
  }
97
- function brandBanner(theme) {
257
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
258
+ function vlen(s) {
259
+ return s.replace(ANSI, "").length;
260
+ }
261
+ var LOGO = [
262
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
263
+ "\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2557",
264
+ " \u2588\u2588\u2554\u255D \u2588\u2588\u2588\u2588\u2588\u2554\u255D",
265
+ " \u2588\u2588\u2554\u255D \u255A\u2550\u2550\u2550\u2588\u2588\u2557",
266
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
267
+ " \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D"
268
+ ];
269
+ var LOGO_W = 16;
270
+ var LOGO_GAP = 6;
271
+ var TIP_ITEMS = [
272
+ { cmd: "/login", desc: "sign in (API key or subscription)" },
273
+ { cmd: "/model", desc: "switch the active model" },
274
+ { cmd: "/hotkeys", desc: "show all keyboard shortcuts" }
275
+ ];
276
+ var ANIM_STYLE = "wipe";
277
+ var ANIM_MS = 750;
278
+ function animateLogoCell(theme, rowText, progress) {
279
+ const accent = (s) => theme.fg("accent", s);
280
+ const glow = (s) => theme.fg("borderAccent", s);
281
+ const logoFull = rowText.padEnd(LOGO_W);
282
+ if (progress >= 1) return accent(logoFull);
283
+ if (ANIM_STYLE === "wipe") {
284
+ const visible = logoFull.slice(0, Math.round(progress * LOGO_W));
285
+ const body = visible.slice(0, Math.max(0, visible.length - 1));
286
+ const edge = visible.slice(Math.max(0, visible.length - 1));
287
+ return `${accent(body)}${glow(edge)}${" ".repeat(LOGO_W - visible.length)}`;
288
+ }
289
+ const band = 2;
290
+ const sweep = progress * (LOGO_W + 2 * band) - band;
291
+ const start = Math.max(0, Math.min(LOGO_W, Math.round(sweep - band)));
292
+ const end = Math.max(0, Math.min(LOGO_W, Math.round(sweep + band) + 1));
293
+ return `${accent(logoFull.slice(0, start))}${glow(logoFull.slice(start, end))}${accent(logoFull.slice(end))}`;
294
+ }
295
+ function boxedHeader(theme, version, width, progress) {
98
296
  const accent = (s) => theme.fg("accent", s);
99
- const muted = (s) => theme.fg("muted", s);
100
297
  const dim = (s) => theme.fg("dim", s);
101
- return [
102
- "",
103
- ` ${accent("\u2588\u258C")} ${accent("Azad73")}`,
104
- ` ${muted("agentic AI \u2014 free to run, yours to own")} ${dim(`\xB7 pi ${VERSION}`)}`,
105
- ""
106
- ];
298
+ const muted = (s) => theme.fg("muted", s);
299
+ const text = (s) => theme.fg("text", s);
300
+ const inner = Math.max(54, Math.min(width - 2, 72));
301
+ const verText = `v${version}`;
302
+ const titlePlain = ` Azad73 ${verText} `;
303
+ const titleColored = ` ${accent("Azad73")} ${dim(verText)} `;
304
+ const dashAfter = Math.max(2, inner - 1 - titlePlain.length);
305
+ const top = `${accent("\u256D\u2500")}${titleColored}${accent("\u2500".repeat(dashAfter))}${accent("\u256E")}`;
306
+ const bottom = accent(`\u2570${"\u2500".repeat(inner)}\u256F`);
307
+ const row = (colored, plainLen) => `${accent("\u2502")}${colored}${" ".repeat(Math.max(0, inner - plainLen))}${accent("\u2502")}`;
308
+ const tipBudget = inner - 2 - LOGO_W - LOGO_GAP;
309
+ const tips = [{ colored: muted("Tips"), plainLen: 4 }];
310
+ for (const t of TIP_ITEMS) {
311
+ const fixed = 2 + t.cmd.length + 1;
312
+ const desc = fixed + t.desc.length > tipBudget ? t.desc.slice(0, Math.max(0, tipBudget - fixed)) : t.desc;
313
+ tips.push({ colored: `${dim("\u2022")} ${accent(t.cmd)} ${text(desc)}`, plainLen: fixed + desc.length });
314
+ }
315
+ const blank = row("", 0);
316
+ const lines = [blank];
317
+ const rowCount = Math.max(LOGO.length, tips.length);
318
+ for (let i = 0; i < rowCount; i++) {
319
+ const cell = animateLogoCell(theme, LOGO[i] ?? "", progress);
320
+ const tip = tips[i] ?? { colored: "", plainLen: 0 };
321
+ lines.push(row(` ${cell}${" ".repeat(LOGO_GAP)}${tip.colored}`, 2 + LOGO_W + LOGO_GAP + tip.plainLen));
322
+ }
323
+ lines.push(blank);
324
+ return ["", top, ...lines, bottom, ""];
325
+ }
326
+ function createBrandingExtension(opts) {
327
+ return (pi) => {
328
+ pi.on("session_start", async (_event, ctx) => {
329
+ if (ctx.mode !== "tui") return;
330
+ ctx.ui.setTitle("Azad73");
331
+ ctx.ui.setHeader((tui, theme) => {
332
+ const start = Date.now();
333
+ let timer;
334
+ const tick = () => {
335
+ if (Date.now() - start >= ANIM_MS && timer) {
336
+ clearInterval(timer);
337
+ timer = void 0;
338
+ }
339
+ tui.requestRender();
340
+ };
341
+ timer = setInterval(tick, 33);
342
+ return {
343
+ render(width) {
344
+ const progress = Math.min(1, (Date.now() - start) / ANIM_MS);
345
+ return boxedHeader(theme, opts.version, width, progress);
346
+ },
347
+ invalidate() {
348
+ },
349
+ dispose() {
350
+ if (timer) clearInterval(timer);
351
+ }
352
+ };
353
+ });
354
+ ctx.ui.setFooter((_tui, theme) => ({
355
+ render(width) {
356
+ const home = os2.homedir();
357
+ const cwd = ctx.cwd.startsWith(home) ? `~${ctx.cwd.slice(home.length)}` : ctx.cwd;
358
+ let cost = 0;
359
+ const entries = ctx.sessionManager.getBranch();
360
+ for (const entry of entries) {
361
+ if (entry.type === "message" && entry.message?.role === "assistant") {
362
+ cost += entry.message.usage?.cost?.total ?? 0;
363
+ }
364
+ }
365
+ const model = ctx.model?.id ?? "no model";
366
+ const level = pi.getThinkingLevel();
367
+ const mode = getApprovalMode();
368
+ const modeText = approvalModeLabel(mode);
369
+ const left = theme.fg("dim", `${cwd} \xB7 $${cost.toFixed(2)} session`);
370
+ const modeColored = mode === "auto" ? theme.fg("warning", modeText) : theme.fg("dim", modeText);
371
+ const right = `${theme.fg("dim", `${model} \xB7 ${level} \xB7 `)}${modeColored}`;
372
+ if (vlen(left) + vlen(right) + 1 > width) {
373
+ return [`${theme.fg("dim", `${model} \xB7 ${level} \xB7 `)}${modeColored}`];
374
+ }
375
+ const gap = Math.max(1, width - vlen(left) - vlen(right));
376
+ return [`${left}${" ".repeat(gap)}${right}`];
377
+ },
378
+ invalidate() {
379
+ }
380
+ }));
381
+ void checkForUpdate(opts.version).then((latest) => {
382
+ if (latest) {
383
+ ctx.ui.notify(`Azad73 update available: ${opts.version} \u2192 ${latest} \xB7 run \`azad update\``, "info");
384
+ }
385
+ });
386
+ });
387
+ };
107
388
  }
108
- var brandingExtension = (pi) => {
109
- pi.on("session_start", async (_event, ctx) => {
110
- if (ctx.mode !== "tui") return;
111
- ctx.ui.setTitle("Azad73");
112
- ctx.ui.setHeader((_tui, theme) => ({
113
- render(_width) {
114
- return brandBanner(theme);
115
- },
116
- invalidate() {
117
- }
118
- }));
119
- });
120
- };
121
389
 
122
390
  // ../engine/src/runtime.ts
123
- async function buildRuntime(workspace = defaultWorkspace(), sessionManager = SessionManager.create(workspace.rootDir)) {
391
+ async function buildRuntime(workspace = defaultWorkspace(), sessionManager = SessionManager.create(workspace.rootDir), options = {}) {
124
392
  const { authStorage, modelRegistry } = createAuth(workspace);
393
+ const branding = createBrandingExtension({ version: options.version ?? "0.0.0" });
125
394
  const createRuntime = async ({ cwd, sessionManager: sm, sessionStartEvent }) => {
395
+ const settingsManager = SettingsManager.create(cwd, workspace.agentDir);
396
+ if (!settingsManager.getQuietStartup()) settingsManager.setQuietStartup(true);
126
397
  const services = await createAgentSessionServices({
127
398
  cwd,
128
399
  agentDir: workspace.agentDir,
129
400
  authStorage,
130
401
  modelRegistry,
402
+ settingsManager,
131
403
  resourceLoaderOptions: {
132
- extensionFactories: [brandingExtension],
404
+ extensionFactories: [branding, createApprovalExtension()],
133
405
  additionalThemePaths: [coreThemesDir()]
134
406
  }
135
407
  });
@@ -147,24 +419,103 @@ async function buildRuntime(workspace = defaultWorkspace(), sessionManager = Ses
147
419
  }
148
420
 
149
421
  // ../engine/src/tui.ts
150
- async function runInteractive(workspace = defaultWorkspace()) {
151
- const runtime = await buildRuntime(workspace);
422
+ async function runInteractive(options = {}) {
423
+ const workspace = options.workspace ?? defaultWorkspace();
424
+ const runtime = await buildRuntime(workspace, void 0, { version: options.version });
152
425
  const mode = new InteractiveMode(runtime);
153
426
  await mode.run();
154
427
  }
155
428
 
429
+ // ../engine/src/workflows.ts
430
+ import fs3 from "node:fs";
431
+ import path5 from "node:path";
432
+ import { parseFrontmatter as parseFrontmatter2 } from "@earendil-works/pi-coding-agent";
433
+ function discoverWorkflows(dirs) {
434
+ const byName = /* @__PURE__ */ new Map();
435
+ for (const dir of dirs) {
436
+ if (!fs3.existsSync(dir)) continue;
437
+ for (const entry of fs3.readdirSync(dir)) {
438
+ if (!entry.endsWith(".md")) continue;
439
+ const filePath = path5.join(dir, entry);
440
+ let content;
441
+ try {
442
+ content = fs3.readFileSync(filePath, "utf8");
443
+ } catch {
444
+ continue;
445
+ }
446
+ const { frontmatter } = parseFrontmatter2(content);
447
+ const name = frontmatter.name?.trim();
448
+ if (!name) continue;
449
+ const steps = (Array.isArray(frontmatter.steps) ? frontmatter.steps : []).map((s) => ({ agent: String(s?.agent ?? "").trim(), task: s?.task })).filter((s) => s.agent.length > 0);
450
+ byName.set(name, {
451
+ name,
452
+ description: frontmatter.description?.trim() ?? "",
453
+ approval: frontmatter.approval === "off" ? "off" : "between-steps",
454
+ steps,
455
+ filePath
456
+ });
457
+ }
458
+ }
459
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
460
+ }
461
+
462
+ // ../engine/src/workflow-run.ts
463
+ import { DefaultResourceLoader, SessionManager as SessionManager2, createAgentSession } from "@earendil-works/pi-coding-agent";
464
+ async function runAgentStep(agent, input, options = {}) {
465
+ const workspace = options.workspace ?? defaultWorkspace();
466
+ const { authStorage, modelRegistry } = createAuth(workspace);
467
+ const model = options.modelId ? modelRegistry.find("anthropic", options.modelId) ?? void 0 : void 0;
468
+ const resourceLoader = new DefaultResourceLoader({
469
+ cwd: workspace.rootDir,
470
+ agentDir: workspace.agentDir,
471
+ systemPromptOverride: () => agent.systemPrompt,
472
+ noExtensions: true
473
+ });
474
+ await resourceLoader.reload();
475
+ const { session } = await createAgentSession({
476
+ cwd: workspace.rootDir,
477
+ agentDir: workspace.agentDir,
478
+ resourceLoader,
479
+ authStorage,
480
+ modelRegistry,
481
+ model,
482
+ tools: agent.tools,
483
+ sessionManager: SessionManager2.inMemory(workspace.rootDir)
484
+ });
485
+ let output = "";
486
+ const unsubscribe = session.subscribe((event) => {
487
+ if (event.type === "message_update" && event.assistantMessageEvent.type === "text_delta") {
488
+ output += event.assistantMessageEvent.delta;
489
+ options.onText?.(event.assistantMessageEvent.delta);
490
+ }
491
+ });
492
+ await session.prompt(input);
493
+ unsubscribe();
494
+ session.dispose();
495
+ return output.trim();
496
+ }
497
+
156
498
  // src/azad.ts
157
499
  import { VERSION as PI_VERSION } from "@earendil-works/pi-coding-agent";
158
500
  var PROVIDER = "anthropic";
159
501
  function coreAgentsDir() {
160
502
  return resolveCoreDataDir("agents", import.meta.url);
161
503
  }
504
+ function azadVersion() {
505
+ try {
506
+ const pkgPath = path6.join(path6.dirname(fileURLToPath2(import.meta.url)), "..", "package.json");
507
+ const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf8"));
508
+ return pkg.version ?? "0.0.0";
509
+ } catch {
510
+ return "0.0.0";
511
+ }
512
+ }
162
513
  function agentDirs() {
163
514
  const ws = defaultWorkspace();
164
- return [coreAgentsDir(), workspaceAgentsDir(), path4.join(ws.rootDir, ".azad", "agents")];
515
+ return [coreAgentsDir(), workspaceAgentsDir(), path6.join(ws.rootDir, ".azad", "agents")];
165
516
  }
166
517
  function workspaceAgentsDir() {
167
- return path4.join(defaultWorkspace().agentDir, "agents");
518
+ return path6.join(defaultWorkspace().agentDir, "agents");
168
519
  }
169
520
  var AGENT_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
170
521
  function newAgent(name) {
@@ -177,12 +528,12 @@ function newAgent(name) {
177
528
  process.exit(1);
178
529
  }
179
530
  const dir = workspaceAgentsDir();
180
- const file = path4.join(dir, `${name}.md`);
181
- if (fs2.existsSync(file)) {
531
+ const file = path6.join(dir, `${name}.md`);
532
+ if (fs4.existsSync(file)) {
182
533
  console.error(`agent already exists: ${file}`);
183
534
  process.exit(1);
184
535
  }
185
- fs2.mkdirSync(dir, { recursive: true });
536
+ fs4.mkdirSync(dir, { recursive: true });
186
537
  const template = [
187
538
  "---",
188
539
  `name: ${name}`,
@@ -195,7 +546,7 @@ function newAgent(name) {
195
546
  "TODO: write the system prompt.",
196
547
  ""
197
548
  ].join("\n");
198
- fs2.writeFileSync(file, template);
549
+ fs4.writeFileSync(file, template);
199
550
  console.log(`created ${file}`);
200
551
  console.log(`edit with: azad agents edit ${name}`);
201
552
  }
@@ -204,16 +555,20 @@ async function editAgent(name) {
204
555
  console.error("usage: azad agents edit <name>");
205
556
  process.exit(1);
206
557
  }
558
+ if (!AGENT_NAME_RE.test(name)) {
559
+ console.error("invalid name: use lowercase letters, digits, and single hyphens (e.g. my-agent)");
560
+ process.exit(1);
561
+ }
207
562
  const dir = workspaceAgentsDir();
208
- const target = path4.join(dir, `${name}.md`);
209
- if (!fs2.existsSync(target)) {
563
+ const target = path6.join(dir, `${name}.md`);
564
+ if (!fs4.existsSync(target)) {
210
565
  const found = discoverAgents(agentDirs()).find((a) => a.name === name);
211
566
  if (!found) {
212
567
  console.error(`agent not found: ${name}`);
213
568
  process.exit(1);
214
569
  }
215
- fs2.mkdirSync(dir, { recursive: true });
216
- fs2.copyFileSync(found.filePath, target);
570
+ fs4.mkdirSync(dir, { recursive: true });
571
+ fs4.copyFileSync(found.filePath, target);
217
572
  console.log(`copied default '${name}' into the workspace for editing: ${target}`);
218
573
  }
219
574
  const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
@@ -265,7 +620,7 @@ function promptLine(question) {
265
620
  }
266
621
  async function authCmd(sub) {
267
622
  const ws = defaultWorkspace();
268
- const authPath = path4.join(ws.agentDir, "auth.json");
623
+ const authPath = path6.join(ws.agentDir, "auth.json");
269
624
  const { authStorage } = createAuth(ws);
270
625
  if (sub === "status" || sub === void 0) {
271
626
  const status = authStorage.getAuthStatus(PROVIDER);
@@ -280,7 +635,7 @@ async function authCmd(sub) {
280
635
  console.error("no key provided");
281
636
  process.exit(1);
282
637
  }
283
- fs2.mkdirSync(ws.agentDir, { recursive: true });
638
+ fs4.mkdirSync(ws.agentDir, { recursive: true });
284
639
  authStorage.set(PROVIDER, { type: "api_key", key });
285
640
  console.log(`stored ${PROVIDER} key in ${authPath}`);
286
641
  return;
@@ -293,6 +648,186 @@ async function authCmd(sub) {
293
648
  console.error("usage: azad auth [status|set|remove]");
294
649
  process.exit(1);
295
650
  }
651
+ function workflowDirs() {
652
+ const ws = defaultWorkspace();
653
+ return [
654
+ resolveCoreDataDir("workflows", import.meta.url),
655
+ path6.join(ws.agentDir, "workflows"),
656
+ path6.join(ws.rootDir, ".azad", "workflows")
657
+ ];
658
+ }
659
+ function workspaceWorkflowsDir() {
660
+ return path6.join(defaultWorkspace().agentDir, "workflows");
661
+ }
662
+ function listWorkflows(workflows) {
663
+ console.log(`Azad73 workflows (${workflows.length}):
664
+ `);
665
+ for (const w of workflows) {
666
+ console.log(` ${w.name.padEnd(14)} ${ellipsis(w.description, 60)}`);
667
+ console.log(` ${" ".repeat(14)} ${w.steps.map((s) => s.agent).join(" \u2192 ")}`);
668
+ }
669
+ }
670
+ function showWorkflow(workflows, name) {
671
+ if (!name) {
672
+ console.error("usage: azad workflow show <name>");
673
+ process.exit(1);
674
+ }
675
+ const workflow = workflows.find((w) => w.name === name);
676
+ if (!workflow) {
677
+ console.error(`workflow not found: ${name}`);
678
+ process.exit(1);
679
+ }
680
+ const known = new Set(discoverAgents(agentDirs()).map((a) => a.name));
681
+ console.log(`# ${workflow.name}`);
682
+ if (workflow.description) console.log(workflow.description);
683
+ console.log(`approval: ${workflow.approval}`);
684
+ console.log(`file: ${workflow.filePath}
685
+ `);
686
+ workflow.steps.forEach((step, i) => {
687
+ const missing = known.has(step.agent) ? "" : " (agent not found)";
688
+ console.log(` ${i + 1}. ${step.agent}${step.task ? ` \u2014 ${step.task}` : ""}${missing}`);
689
+ });
690
+ }
691
+ function newWorkflow(name) {
692
+ if (!name) {
693
+ console.error("usage: azad workflow new <name>");
694
+ process.exit(1);
695
+ }
696
+ if (!AGENT_NAME_RE.test(name)) {
697
+ console.error("invalid name: use lowercase letters, digits, and single hyphens (e.g. my-flow)");
698
+ process.exit(1);
699
+ }
700
+ const dir = workspaceWorkflowsDir();
701
+ const file = path6.join(dir, `${name}.md`);
702
+ if (fs4.existsSync(file)) {
703
+ console.error(`workflow already exists: ${file}`);
704
+ process.exit(1);
705
+ }
706
+ fs4.mkdirSync(dir, { recursive: true });
707
+ const template = [
708
+ "---",
709
+ `name: ${name}`,
710
+ "description: TODO \u2014 what this workflow does.",
711
+ "approval: between-steps",
712
+ "steps:",
713
+ " - agent: context-cartographer",
714
+ "---",
715
+ "",
716
+ "TODO: describe the workflow. Add or remove steps under `steps:` (each is `- agent: <name>`).",
717
+ ""
718
+ ].join("\n");
719
+ fs4.writeFileSync(file, template);
720
+ console.log(`created ${file}`);
721
+ console.log(`edit with: azad workflow edit ${name}`);
722
+ }
723
+ async function editWorkflow(name) {
724
+ if (!name) {
725
+ console.error("usage: azad workflow edit <name>");
726
+ process.exit(1);
727
+ }
728
+ if (!AGENT_NAME_RE.test(name)) {
729
+ console.error("invalid name: use lowercase letters, digits, and single hyphens");
730
+ process.exit(1);
731
+ }
732
+ const dir = workspaceWorkflowsDir();
733
+ const target = path6.join(dir, `${name}.md`);
734
+ if (!fs4.existsSync(target)) {
735
+ const found = discoverWorkflows(workflowDirs()).find((w) => w.name === name);
736
+ if (!found) {
737
+ console.error(`workflow not found: ${name}`);
738
+ process.exit(1);
739
+ }
740
+ fs4.mkdirSync(dir, { recursive: true });
741
+ fs4.copyFileSync(found.filePath, target);
742
+ console.log(`copied default '${name}' into the workspace for editing: ${target}`);
743
+ }
744
+ const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
745
+ await new Promise((resolve, reject) => {
746
+ const child = spawn(editor, [target], { stdio: "inherit" });
747
+ child.on("exit", () => resolve());
748
+ child.on("error", reject);
749
+ });
750
+ }
751
+ function buildStepInput(step, goal, previous, index) {
752
+ let task = step.task ?? "";
753
+ if (task.includes("{previous}")) task = task.replace(/\{previous\}/g, previous);
754
+ if (index === 0) return task || goal || "Begin.";
755
+ const parts = [];
756
+ if (task) parts.push(task);
757
+ if (goal) parts.push(`Overall goal: ${goal}`);
758
+ parts.push(`Output from the previous step:
759
+
760
+ ${previous}`);
761
+ return parts.join("\n\n");
762
+ }
763
+ async function promptYesNo(question) {
764
+ const answer = (await promptLine(question)).trim().toLowerCase();
765
+ return answer === "" || answer === "y" || answer === "yes";
766
+ }
767
+ async function runWorkflow(name, goal, modelId) {
768
+ if (!name) {
769
+ console.error("usage: azad workflow run <name> [goal] [--model <id>]");
770
+ process.exit(1);
771
+ }
772
+ const workflow = discoverWorkflows(workflowDirs()).find((w) => w.name === name);
773
+ if (!workflow) {
774
+ console.error(`workflow not found: ${name}`);
775
+ process.exit(1);
776
+ }
777
+ const agentMap = new Map(discoverAgents(agentDirs()).map((a) => [a.name, a]));
778
+ const missing = [...new Set(workflow.steps.filter((s) => !agentMap.has(s.agent)).map((s) => s.agent))];
779
+ if (missing.length > 0) {
780
+ console.error(`workflow '${name}' references unknown agents: ${missing.join(", ")}`);
781
+ process.exit(1);
782
+ }
783
+ console.log(
784
+ `
785
+ Workflow: ${workflow.name} \xB7 ${workflow.steps.length} steps \xB7 approval: ${workflow.approval}${modelId ? ` \xB7 model: ${modelId}` : ""}
786
+ `
787
+ );
788
+ let previous = "";
789
+ for (let i = 0; i < workflow.steps.length; i++) {
790
+ const step = workflow.steps[i];
791
+ const agent = step ? agentMap.get(step.agent) : void 0;
792
+ if (!step || !agent) continue;
793
+ if (i > 0 && workflow.approval === "between-steps") {
794
+ const ok = await promptYesNo(`Proceed to step ${i + 1}/${workflow.steps.length} \xB7 ${step.agent}? [Y/n] `);
795
+ if (!ok) {
796
+ console.log("\nWorkflow stopped.");
797
+ return;
798
+ }
799
+ }
800
+ console.log(`
801
+ \u2500\u2500\u2500 Step ${i + 1}/${workflow.steps.length} \xB7 ${step.agent} \u2500\u2500\u2500
802
+ `);
803
+ previous = await runAgentStep(agent, buildStepInput(step, goal, previous, i), {
804
+ modelId,
805
+ onText: (t) => process.stdout.write(t)
806
+ });
807
+ process.stdout.write("\n");
808
+ }
809
+ console.log(`
810
+ \u2713 Workflow '${workflow.name}' complete.
811
+ `);
812
+ }
813
+ async function update() {
814
+ const current = azadVersion();
815
+ process.stdout.write("Checking npm for the latest version\u2026 ");
816
+ const latest = await getLatestVersion();
817
+ if (!latest) {
818
+ console.log("could not reach the registry.");
819
+ process.exit(1);
820
+ }
821
+ if (!isNewerVersion(latest, current)) {
822
+ console.log(`already up to date (v${current}).`);
823
+ return;
824
+ }
825
+ console.log(`updating ${current} \u2192 ${latest}`);
826
+ const child = spawn("npm", ["install", "-g", `${CLI_PACKAGE}@latest`], { stdio: "inherit" });
827
+ const code = await new Promise((resolve) => child.on("exit", (c) => resolve(c ?? 0)));
828
+ if (code === 0) console.log(`Updated to v${latest}.`);
829
+ process.exit(code);
830
+ }
296
831
  function printHelp() {
297
832
  console.log(
298
833
  [
@@ -304,8 +839,14 @@ function printHelp() {
304
839
  " azad agents show <name> show one agent",
305
840
  " azad agents new <name> scaffold a new agent in ~/.azad/agents",
306
841
  " azad agents edit <name> edit an agent ($EDITOR; copies a default into the workspace)",
842
+ " azad workflow list list workflows",
843
+ " azad workflow show <name> show a workflow's steps",
844
+ " azad workflow new <name> scaffold a workflow in ~/.azad/workflows",
845
+ " azad workflow edit <name> edit a workflow (add/remove agents under steps:)",
846
+ " azad workflow run <name> run a workflow (--model <id> to override the model)",
307
847
  " azad auth status|set|remove manage the Anthropic credential (~/.azad/auth.json)",
308
- " azad version print version"
848
+ " azad version print version",
849
+ " azad update update to the latest published version"
309
850
  ].join("\n")
310
851
  );
311
852
  }
@@ -313,12 +854,18 @@ async function main() {
313
854
  const [cmd, sub, arg] = process.argv.slice(2);
314
855
  switch (cmd) {
315
856
  case void 0:
316
- await runInteractive();
857
+ await runInteractive({ version: azadVersion() });
317
858
  return;
318
859
  case "version":
319
860
  case "--version":
320
- case "-v":
321
- console.log(`Azad73 (azad) \u2014 Pi SDK ${PI_VERSION}`);
861
+ case "-v": {
862
+ console.log(`Azad73 v${azadVersion()} (Pi SDK ${PI_VERSION})`);
863
+ const latest = cachedUpdate(azadVersion());
864
+ if (latest) console.log(`Update available: ${azadVersion()} \u2192 ${latest} \xB7 run \`azad update\``);
865
+ return;
866
+ }
867
+ case "update":
868
+ await update();
322
869
  return;
323
870
  case "help":
324
871
  case "--help":
@@ -339,6 +886,37 @@ async function main() {
339
886
  else listAgents(agents);
340
887
  return;
341
888
  }
889
+ case "workflow":
890
+ case "workflows": {
891
+ if (sub === "new") {
892
+ newWorkflow(arg);
893
+ return;
894
+ }
895
+ if (sub === "edit") {
896
+ await editWorkflow(arg);
897
+ return;
898
+ }
899
+ if (sub === "run") {
900
+ const rest = process.argv.slice(5);
901
+ let modelId;
902
+ const goalParts = [];
903
+ for (let i = 0; i < rest.length; i++) {
904
+ if (rest[i] === "--model") {
905
+ modelId = rest[i + 1];
906
+ i++;
907
+ } else {
908
+ const part = rest[i];
909
+ if (part) goalParts.push(part);
910
+ }
911
+ }
912
+ await runWorkflow(arg, goalParts.join(" "), modelId);
913
+ return;
914
+ }
915
+ const workflows = discoverWorkflows(workflowDirs());
916
+ if (sub === "show") showWorkflow(workflows, arg);
917
+ else listWorkflows(workflows);
918
+ return;
919
+ }
342
920
  case "auth":
343
921
  await authCmd(sub);
344
922
  return;
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: bugfix
3
+ description: Bug lifecycle — reproduce, find root cause, fix, review, document.
4
+ approval: between-steps
5
+ steps:
6
+ - agent: bug-reproduction-helper
7
+ - agent: root-cause-investigator
8
+ - agent: bug-fixer
9
+ - agent: bugfix-mr-reviewer
10
+ - agent: ticket-doc-writer
11
+ ---
12
+
13
+ Run on a confirmed bug. Each step passes its output to the next.
@@ -0,0 +1,11 @@
1
+ ---
2
+ name: feature
3
+ description: Feature lifecycle — build, review, document.
4
+ approval: between-steps
5
+ steps:
6
+ - agent: feature-builder
7
+ - agent: feature-mr-reviewer
8
+ - agent: ticket-doc-writer
9
+ ---
10
+
11
+ Run from acceptance criteria. Each step passes its output to the next.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@azad-73/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Azad73 — agentic AI platform CLI (branded TUI + agents) built on the Pi SDK",
5
- "license": "MIT",
5
+ "license": "UNLICENSED",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "azad": "./dist/azad.js"