avenic 1.0.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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -0
  3. package/package.json +30 -0
  4. package/scripts/skills.mjs +13 -0
  5. package/scripts/watchdog.mjs +50 -0
  6. package/src/cli/dispatcher.mjs +439 -0
  7. package/src/cli/self-update.mjs +27 -0
  8. package/src/cli/skills-cli.mjs +1060 -0
  9. package/src/cli/watchdog.mjs +30 -0
  10. package/vendor/core-src/index.mjs +143 -0
  11. package/vendor/core-src/runtime/adapters/claude.mjs +81 -0
  12. package/vendor/core-src/runtime/adapters/codex.mjs +142 -0
  13. package/vendor/core-src/runtime/adapters/index.mjs +9 -0
  14. package/vendor/core-src/runtime/adapters/opencode.mjs +76 -0
  15. package/vendor/core-src/runtime/agents.mjs +39 -0
  16. package/vendor/core-src/runtime/config.mjs +227 -0
  17. package/vendor/core-src/runtime/gitignore.mjs +69 -0
  18. package/vendor/core-src/runtime/process.mjs +41 -0
  19. package/vendor/core-src/runtime/project-root.mjs +42 -0
  20. package/vendor/core-src/runtime/sessions.mjs +312 -0
  21. package/vendor/core-src/skills/catalog.mjs +130 -0
  22. package/vendor/core-src/skills/direct.mjs +209 -0
  23. package/vendor/core-src/skills/git.mjs +93 -0
  24. package/vendor/core-src/skills/ids.mjs +40 -0
  25. package/vendor/core-src/skills/install.mjs +357 -0
  26. package/vendor/core-src/skills/packs.mjs +256 -0
  27. package/vendor/core-src/skills/paths.mjs +92 -0
  28. package/vendor/core-src/skills/sources.mjs +246 -0
  29. package/vendor/core-src/skills/ui.mjs +21 -0
  30. package/vendor/core-src/skills/vendor.mjs +57 -0
  31. package/vendor/core-src/util/fail.mjs +3 -0
  32. package/vendor/core-src/util/fs.mjs +14 -0
  33. package/vendor/core-src/util/json.mjs +14 -0
@@ -0,0 +1,439 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ AGENTS,
6
+ acquireSessionLease,
7
+ agentExecutableAvailable,
8
+ clearLocalAuth,
9
+ deinitializeAgent,
10
+ effectiveAgentConfig,
11
+ getAgent,
12
+ getSessionAdapter,
13
+ initializeAgent,
14
+ loadRuntime,
15
+ locateProjectRoot,
16
+ projectAuthEnvironment,
17
+ sessionLeasePath,
18
+ sessionsGitIgnored,
19
+ setLocalAuth,
20
+ setSessionsGitIgnored,
21
+ spawnExecutableSync,
22
+ validateAuthMode,
23
+ validateSessionsMode,
24
+ } from "#core";
25
+ import { dispatchCatalog, dispatchSkills } from "./skills-cli.mjs";
26
+ import { updateAvenic } from "./self-update.mjs";
27
+ import { spawnSessionWatchdog } from "./watchdog.mjs";
28
+
29
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
30
+
31
+ function launchExecutable(executable, argumentsList, options = {}) {
32
+ const result = spawnExecutableSync(executable, argumentsList, {
33
+ cwd: options.cwd,
34
+ env: options.environment,
35
+ stdio: options.capture ? "pipe" : "inherit",
36
+ windowsHide: Boolean(options.capture),
37
+ });
38
+ if (result.error) {
39
+ throw new Error(`Unable to launch ${executable}: ${result.error.message}`);
40
+ }
41
+ return result.status ?? 1;
42
+ }
43
+
44
+ function takeOption(argumentsList, option) {
45
+ const index = argumentsList.indexOf(option);
46
+ if (index === -1) {
47
+ return undefined;
48
+ }
49
+ const value = argumentsList[index + 1];
50
+ if (!value || value.startsWith("-")) {
51
+ throw new Error(`Missing value for ${option}`);
52
+ }
53
+ argumentsList.splice(index, 2);
54
+ return value;
55
+ }
56
+
57
+ export function printHelp(io = console) {
58
+ io.log(`Avenic
59
+
60
+ CLI: avenic (shorthand: ave)
61
+
62
+ Agent runtimes:
63
+ avenic <claude|codex|opencode> init [--auth global|project] [--sessions global|project]
64
+ avenic <claude|codex|opencode> deinit [--purge]
65
+ avenic <claude|codex|opencode> auth [global|project|reset]
66
+ avenic <claude|codex|opencode> status
67
+ avenic <claude|codex|opencode> sessions [import|writeback|status]
68
+ avenic <claude|codex|opencode> [official CLI arguments...]
69
+ avenic sessions git [on|off|status]
70
+ avenic status Show all three agents
71
+ avenic doctor Check the environment
72
+
73
+ Skills:
74
+ avenic skills install [pack...] Install or sync Packs (default: common)
75
+ avenic skills [pack...] Shorthand for skills install
76
+ avenic skills add <owner/repo> [skill...] [-g] Install directly from a GitHub repo
77
+ avenic skills remove <skill...> Remove external, unmanaged Skills
78
+ avenic skills uninstall <pack...> Remove Packs and unneeded managed Skills
79
+ avenic skills uninstall Remove all managed Skills
80
+ avenic skills tree [pack...] Show source -> Skill tree
81
+ avenic skills packs List available Packs
82
+ avenic skills status [-g] Show the installed tree
83
+ -g, --global Use the global user scope
84
+
85
+ Catalog:
86
+ avenic catalog add <spec> Add a catalog source (owner/repo[#ref], URL, or local path) and preview its Packs
87
+ avenic catalog select [name|spec] Pick the current catalog from registered ones (↑/↓, Enter)
88
+ avenic catalog list List registered catalogs
89
+ avenic catalog sync Fetch or update the cached catalog
90
+ avenic catalog default Show the configured catalog spec
91
+ Private repos use your local git credentials (gh auth login or SSH)
92
+ avenic catalog doctor|update|skill-add|remove|pack-add|pack-remove|source-add
93
+ (run inside your catalog Git clone)
94
+
95
+ Update Avenic:
96
+ avenic self-update
97
+ `);
98
+ }
99
+
100
+ function printAgentStatus(agent, projectRoot, state) {
101
+ const config = effectiveAgentConfig(state, agent.id);
102
+ console.log(`${agent.displayName}\n`);
103
+ console.log(`Project ${projectRoot}`);
104
+ console.log(`Initialized ${config ? "Yes" : "No"}`);
105
+ if (config) {
106
+ console.log(`Configured auth ${config.configuredAuth}`);
107
+ console.log(`Local override ${config.localAuth ?? "None"}`);
108
+ console.log(`Effective auth ${config.auth}`);
109
+ console.log(`Sessions ${config.sessions === "global" ? "Global (native)" : "Project (portable)"}`);
110
+ }
111
+ console.log(`Official CLI ${agentExecutableAvailable(agent.id) ? "Available" : "Not found"}`);
112
+ }
113
+
114
+ async function dispatchAgent(agentId, argumentsList) {
115
+ const agent = getAgent(agentId);
116
+ const projectRoot = locateProjectRoot();
117
+ const [command, ...remainingArguments] = argumentsList;
118
+
119
+ if (command === "help" || command === "--help" || command === "-h") {
120
+ printHelp();
121
+ return 0;
122
+ }
123
+
124
+ if (command === "init") {
125
+ const initArguments = [...remainingArguments];
126
+ const authOption = takeOption(initArguments, "--auth");
127
+ const authMode = authOption ? validateAuthMode(authOption) : undefined;
128
+ const sessionsOption = takeOption(initArguments, "--sessions");
129
+ const sessionsMode = sessionsOption ? validateSessionsMode(sessionsOption) : undefined;
130
+ if (initArguments.length > 0) {
131
+ throw new Error(`Unknown option: ${initArguments[0]}`);
132
+ }
133
+ const result = await initializeAgent(projectRoot, agentId, authMode, sessionsMode);
134
+ console.log("Avenic Runtime\n");
135
+ console.log(`Agent ${agent.displayName}`);
136
+ console.log(`Project ${projectRoot}`);
137
+ console.log(`Authentication ${result.authMode}`);
138
+ console.log(`Sessions ${result.sessionsMode === "global" ? "Global" : "Project"}`);
139
+ console.log(`Session Git ${(await sessionsGitIgnored(projectRoot)) ? "Off" : "On"}`);
140
+ console.log(`Configuration ${result.configChanged ? "Updated" : "Unchanged"}`);
141
+ console.log(`Git ignore ${result.gitignoreChanged ? "Updated" : "Unchanged"}`);
142
+ console.log(`Structure ${result.structureRepaired ? "Repaired" : "Intact"}`);
143
+ const changedSomething = result.configChanged || result.gitignoreChanged || result.structureRepaired;
144
+ console.log(changedSomething ? "\nChanged:" : "\nAlready up to date — nothing changed.");
145
+ if (result.configChanged) {
146
+ console.log(" .agents/runtime.json Runtime config (agent, auth, sessions)");
147
+ }
148
+ if (result.gitignoreChanged) {
149
+ console.log(" .gitignore Added ignore rules for .agents/ and .claude/skills/");
150
+ }
151
+ if (result.structureRepaired) {
152
+ console.log(` .agents/sessions/${agentId}/ Portable sessions (in Git by default)`);
153
+ if (result.authMode === "project") {
154
+ console.log(` .agents/local/${agentId}/ Project credentials (gitignored)`);
155
+ }
156
+ }
157
+ console.log(`\nUsage:
158
+ avenic ${agentId} Launch ${agent.displayName}
159
+ avenic ${agentId} status Show configuration
160
+ avenic ${agentId} deinit Undo init (--purge also deletes data)
161
+ avenic ${agentId} auth Switch global/project authentication\n`);
162
+ console.log(
163
+ "Project sessions may contain prompts, source code, command output, file paths, and secrets. Only commit sessions to repositories you trust.\n",
164
+ );
165
+ return 0;
166
+ }
167
+
168
+ if (command === "deinit") {
169
+ const purge = remainingArguments.includes("--purge");
170
+ const unknown = remainingArguments.find((argument) => argument !== "--purge");
171
+ if (unknown) {
172
+ throw new Error(`Unknown option: ${unknown}`);
173
+ }
174
+ const result = await deinitializeAgent(projectRoot, agentId, { purge });
175
+ console.log(`${agent.displayName} deinitialization\n`);
176
+ console.log(`Project ${projectRoot}`);
177
+ console.log(`Runtime ${result.changed ? "Removed" : "Already absent"}`);
178
+ console.log(`Data ${result.purged ? "Purged" : "Preserved"}`);
179
+ console.log(`Agents ${result.remaining} remaining`);
180
+ if (!purge) {
181
+ console.log(`\nReinitialize later without losing portable sessions:\n avenic ${agentId} init`);
182
+ }
183
+ return 0;
184
+ }
185
+
186
+ if (command === "auth") {
187
+ if (remainingArguments.length === 0) {
188
+ printAgentStatus(agent, projectRoot, await loadRuntime(projectRoot));
189
+ return 0;
190
+ }
191
+ if (remainingArguments.length !== 1) {
192
+ throw new Error(`Usage: avenic ${agentId} auth [global|project|reset]`);
193
+ }
194
+ const mode = remainingArguments[0];
195
+ const config = mode === "reset"
196
+ ? await clearLocalAuth(projectRoot, agentId)
197
+ : await setLocalAuth(projectRoot, agentId, mode);
198
+ console.log(`${agent.displayName} authentication\n`);
199
+ console.log(`Configured default ${config.configuredAuth}`);
200
+ console.log(`Local override ${config.localAuth ?? "None"}`);
201
+ console.log(`Effective ${config.auth}`);
202
+ return 0;
203
+ }
204
+
205
+ if (command === "status") {
206
+ printAgentStatus(agent, projectRoot, await loadRuntime(projectRoot));
207
+ return 0;
208
+ }
209
+
210
+ if (command === "sessions") {
211
+ const state = await loadRuntime(projectRoot);
212
+ if (!effectiveAgentConfig(state, agentId)) {
213
+ throw new Error(`${agent.displayName} is not initialized. Run: avenic ${agentId} init`);
214
+ }
215
+ const action = remainingArguments[0] ?? "status";
216
+ if (remainingArguments.length > 1 || !["import", "writeback", "status"].includes(action)) {
217
+ throw new Error(`Usage: avenic ${agentId} sessions [import|writeback|status]`);
218
+ }
219
+ const adapter = getSessionAdapter(agentId);
220
+ if (action === "status") {
221
+ const result = await adapter.status(projectRoot);
222
+ console.log(`${agent.displayName} portable sessions\n\nProject ${projectRoot}\nSessions ${result.count}`);
223
+ return 0;
224
+ }
225
+ const result = action === "import" ? await adapter.capture(projectRoot) : await adapter.restore(projectRoot);
226
+ console.log(`${agent.displayName} session ${action}\n`);
227
+ console.log(`Project ${projectRoot}`);
228
+ console.log(`Sessions ${result.count}`);
229
+ console.log(action === "import" ? `Portable ${result.changed ? "Updated" : "Unchanged"}` : `Written back ${result.added + result.updated}`);
230
+ if (result.conflicts > 0) {
231
+ console.log(`Conflicts ${result.conflicts} (project sessions overwrote native storage)`);
232
+ }
233
+ return 0;
234
+ }
235
+
236
+ const state = await loadRuntime(projectRoot);
237
+ const config = effectiveAgentConfig(state, agentId);
238
+ if (!config) {
239
+ throw new Error(`${agent.displayName} is not initialized. Run: avenic ${agentId} init`);
240
+ }
241
+ const environment = config.auth === "project"
242
+ ? { ...process.env, ...projectAuthEnvironment(agentId, projectRoot) }
243
+ : process.env;
244
+ const adapter = getSessionAdapter(agentId);
245
+ const portableSessions = config.sessions !== "global";
246
+ // Sessions created during a run live only in the project: the first launch
247
+ // of a project+agent group snapshots the native storage and the last exit
248
+ // reverts it. Launches of the same project+agent may run concurrently.
249
+ // opencode's storage is managed by the official CLI, so it captures without
250
+ // snapshotting or reverting.
251
+ const isolatesNative = typeof adapter.snapshotNative === "function"
252
+ && typeof adapter.revertNative === "function";
253
+ let leaveLaunchGroup = null;
254
+ if (portableSessions && isolatesNative) {
255
+ const snapshotRoot = path.join(sessionLeasePath(agentId, projectRoot), "snapshot");
256
+ const lease = await acquireSessionLease(agentId, projectRoot, {
257
+ onFirst: async (recovering) => {
258
+ if (recovering) {
259
+ // A previous launch group died without exiting: move its sessions
260
+ // into the project and restore the pre-launch native state first.
261
+ await adapter.capture(projectRoot, { environment });
262
+ await adapter.revertNative(snapshotRoot, projectRoot, { environment });
263
+ }
264
+ await adapter.snapshotNative(projectRoot, snapshotRoot, { environment });
265
+ },
266
+ onLast: async () => {
267
+ await adapter.revertNative(snapshotRoot, projectRoot, { environment });
268
+ },
269
+ });
270
+ leaveLaunchGroup = lease.release;
271
+ try {
272
+ await spawnSessionWatchdog(agentId, projectRoot, lease.member, environment);
273
+ } catch {}
274
+ }
275
+ if (portableSessions) {
276
+ // Project session records take priority on launch: conflicting native
277
+ // copies are overwritten silently. Native storage is never written to
278
+ // proactively; only `avenic <agent> sessions writeback` writes
279
+ // project records back to native storage.
280
+ try {
281
+ await adapter.restore(projectRoot, { environment });
282
+ } catch (error) {
283
+ if (leaveLaunchGroup) {
284
+ // Leaving the group reverts native storage when this was the only
285
+ // launch in it.
286
+ try {
287
+ await leaveLaunchGroup();
288
+ } catch {}
289
+ }
290
+ throw error;
291
+ }
292
+ }
293
+ let status;
294
+ try {
295
+ status = launchExecutable(agent.executable, argumentsList, { cwd: projectRoot, environment });
296
+ } finally {
297
+ if (portableSessions) {
298
+ try {
299
+ await adapter.capture(projectRoot, { environment });
300
+ } finally {
301
+ if (leaveLaunchGroup) {
302
+ await leaveLaunchGroup();
303
+ }
304
+ }
305
+ }
306
+ }
307
+ return status;
308
+ }
309
+
310
+ async function dispatchStatus() {
311
+ const projectRoot = locateProjectRoot();
312
+ const state = await loadRuntime(projectRoot);
313
+ console.log("Avenic Status\n");
314
+ console.log(`Project ${projectRoot}\n`);
315
+ for (const agentId of Object.keys(AGENTS)) {
316
+ const agent = getAgent(agentId);
317
+ const config = effectiveAgentConfig(state, agentId);
318
+ console.log(`${agent.displayName.padEnd(12)} ${config ? `Initialized (${config.auth} auth)` : "Not initialized"}`);
319
+ }
320
+ return 0;
321
+ }
322
+
323
+ async function dispatchDoctor() {
324
+ const projectRoot = locateProjectRoot();
325
+ const state = await loadRuntime(projectRoot);
326
+ console.log("Avenic Doctor\n");
327
+ console.log(`Project root OK ${projectRoot}`);
328
+ console.log(`Runtime config ${state.runtime.agents ? "OK" : "ERROR"}`);
329
+ for (const agentId of Object.keys(AGENTS)) {
330
+ const agent = getAgent(agentId);
331
+ console.log(`${agent.displayName.padEnd(18)} ${agentExecutableAvailable(agent.id) ? "OK" : "NOT FOUND"}`);
332
+ }
333
+ return 0;
334
+ }
335
+
336
+ function untrackSessions(projectRoot) {
337
+ const tracked = spawnExecutableSync("git", ["ls-files", "--", ".agents/sessions"], {
338
+ cwd: projectRoot,
339
+ encoding: "utf8",
340
+ windowsHide: true,
341
+ });
342
+ if (tracked.status !== 0 || !tracked.stdout.trim()) return 0;
343
+ const files = tracked.stdout.trim().split(/\r?\n/).filter(Boolean);
344
+ const result = spawnExecutableSync(
345
+ "git",
346
+ ["rm", "-r", "--cached", "--force", "--ignore-unmatch", "--", ".agents/sessions"],
347
+ { cwd: projectRoot, stdio: "inherit" },
348
+ );
349
+ if (result.status !== 0) throw new Error("Unable to remove sessions from the Git index");
350
+ return files.length;
351
+ }
352
+
353
+ async function dispatchSessions(argumentsList) {
354
+ const [command, mode = "status", ...extra] = argumentsList;
355
+ if (command !== "git" || extra.length > 0 || !["on", "off", "status"].includes(mode)) {
356
+ throw new Error("Usage: avenic sessions git [on|off|status]");
357
+ }
358
+ const projectRoot = locateProjectRoot();
359
+ if (mode === "off") {
360
+ const changed = await setSessionsGitIgnored(projectRoot, true);
361
+ const untracked = untrackSessions(projectRoot);
362
+ console.log("Session Git sync\n");
363
+ console.log(`Project ${projectRoot}`);
364
+ console.log("Status Off");
365
+ console.log(`Git ignore ${changed ? "Updated" : "Unchanged"}`);
366
+ console.log(`Untracked ${untracked}`);
367
+ return 0;
368
+ }
369
+ if (mode === "on") {
370
+ const changed = await setSessionsGitIgnored(projectRoot, false);
371
+ console.log("Session Git sync\n");
372
+ console.log(`Project ${projectRoot}`);
373
+ console.log("Status On");
374
+ console.log(`Git ignore ${changed ? "Updated" : "Unchanged"}`);
375
+ return 0;
376
+ }
377
+ console.log(`Session Git sync: ${(await sessionsGitIgnored(projectRoot)) ? "Off" : "On"}`);
378
+ return 0;
379
+ }
380
+
381
+ async function dispatchSkillsCommand(argumentsList) {
382
+ return dispatchSkills(argumentsList, {
383
+ io: console,
384
+ cwd: process.cwd(),
385
+ environment: process.env,
386
+ });
387
+ }
388
+
389
+ export async function runCli(options = {}) {
390
+ const argumentsList = options.argumentsList ?? process.argv.slice(2);
391
+ const forcedAgent = options.forcedAgent;
392
+ if (forcedAgent) {
393
+ return dispatchAgent(forcedAgent, argumentsList);
394
+ }
395
+ const [command, ...remainingArguments] = argumentsList;
396
+ if (!command || command === "help" || command === "--help" || command === "-h") {
397
+ printHelp();
398
+ return 0;
399
+ }
400
+ if (Object.hasOwn(AGENTS, command)) {
401
+ return dispatchAgent(command, remainingArguments);
402
+ }
403
+ if (command === "skills") {
404
+ return dispatchSkillsCommand(remainingArguments);
405
+ }
406
+ if (command === "catalog") {
407
+ return dispatchCatalog(remainingArguments, {
408
+ io: console,
409
+ cwd: process.cwd(),
410
+ environment: process.env,
411
+ });
412
+ }
413
+ if (command === "sessions") {
414
+ return dispatchSessions(remainingArguments);
415
+ }
416
+ if (command === "update") {
417
+ if (remainingArguments.length > 0) {
418
+ throw new Error("Usage: avenic self-update");
419
+ }
420
+ await updateAvenic(packageRoot);
421
+ return 0;
422
+ }
423
+ if (command === "status") {
424
+ return dispatchStatus();
425
+ }
426
+ if (command === "doctor") {
427
+ return dispatchDoctor();
428
+ }
429
+ // Anything left is either a legacy top-level command (add, self-update,
430
+ // uninstall, packs, tree, ...) or a Pack id. Pack ids are user-defined, so
431
+ // the catalog is the only source of truth for telling Packs from typos;
432
+ // delegate to the skills dispatcher, which resolves known commands locally
433
+ // before any catalog work.
434
+ return dispatchSkills(argumentsList, {
435
+ io: console,
436
+ cwd: process.cwd(),
437
+ environment: process.env,
438
+ });
439
+ }
@@ -0,0 +1,27 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawnExecutableSync } from "#core";
4
+
5
+ export async function avenicPackageSpec(packageRoot) {
6
+ const metadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
7
+ return metadata.avenic?.packageSpec ?? "Echo-Kang-hub/avenic#main";
8
+ }
9
+
10
+ export async function updateAvenic(packageRoot, options = {}) {
11
+ const packageSpec = await avenicPackageSpec(packageRoot);
12
+ console.log("Updating Avenic");
13
+ console.log(`Source: ${packageSpec}\n`);
14
+ const result = (options.spawn ?? spawnExecutableSync)(
15
+ "npm",
16
+ ["install", "--global", packageSpec],
17
+ { stdio: "inherit" },
18
+ );
19
+ if (result.error) {
20
+ throw new Error(`Unable to launch npm: ${result.error.message}`);
21
+ }
22
+ if (result.status !== 0) {
23
+ throw new Error(`npm install failed with exit code ${result.status ?? 1}`);
24
+ }
25
+ console.log("\nAvenic update complete");
26
+ return packageSpec;
27
+ }