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,30 @@
1
+ import { spawn } from "node:child_process";
2
+ import { writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { sessionLeasePath } from "#core";
7
+
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
9
+
10
+ // Start the detached watchdog that finishes this launch if the CLI process is
11
+ // killed without a normal exit (closed terminal, closed editor, crash): it
12
+ // captures the run's sessions into the project and restores the native
13
+ // storage. Detached and windowless, so closing the terminal does not kill it.
14
+ // Best-effort: launch continues without it if spawning fails.
15
+ export async function spawnSessionWatchdog(agentId, projectRoot, member, environment) {
16
+ const stateDir = sessionLeasePath(agentId, projectRoot);
17
+ await writeFile(
18
+ path.join(stateDir, "watchdog.json"),
19
+ JSON.stringify({ member, parentPid: process.pid, agentId, projectRoot, environment }),
20
+ { encoding: "utf8", mode: 0o600 },
21
+ );
22
+ const child = spawn(process.execPath, [path.join(packageRoot, "scripts", "watchdog.mjs"), stateDir], {
23
+ cwd: os.tmpdir(),
24
+ detached: true,
25
+ stdio: "ignore",
26
+ windowsHide: true,
27
+ });
28
+ child.unref();
29
+ return child;
30
+ }
@@ -0,0 +1,143 @@
1
+ // Public API of @avenic/core.
2
+
3
+ export { AGENTS, agentExecutableAvailable, getAgent } from "./runtime/agents.mjs";
4
+ export {
5
+ clearLocalAuth,
6
+ deinitializeAgent,
7
+ effectiveAgentConfig,
8
+ initializeAgent,
9
+ loadRuntime,
10
+ projectAuthEnvironment,
11
+ runtimePaths,
12
+ setLocalAuth,
13
+ validateAuthMode,
14
+ validateSessionsMode,
15
+ } from "./runtime/config.mjs";
16
+ export {
17
+ REQUIRED_RULES,
18
+ SESSIONS_RULE,
19
+ ensureRuntimeGitignore,
20
+ removeRuntimeGitignore,
21
+ sessionsGitIgnored,
22
+ setSessionsGitIgnored,
23
+ } from "./runtime/gitignore.mjs";
24
+ export { locateProjectRoot } from "./runtime/project-root.mjs";
25
+ export { spawnExecutableSync } from "./runtime/process.mjs";
26
+ export {
27
+ PROJECT_ROOT_TOKEN,
28
+ acquireSessionLease,
29
+ hashContent,
30
+ listFiles,
31
+ mergeFiles,
32
+ processAlive,
33
+ readFirstJsonLine,
34
+ releaseSessionLease,
35
+ replaceDirectory,
36
+ revertFrom,
37
+ samePath,
38
+ sessionLeasePath,
39
+ snapshotFiles,
40
+ snapshotInto,
41
+ transformJsonLines,
42
+ } from "./runtime/sessions.mjs";
43
+ export { getSessionAdapter } from "./runtime/adapters/index.mjs";
44
+
45
+ export { fail } from "./util/fail.mjs";
46
+ export { isInside, removeEmptyDirectory } from "./util/fs.mjs";
47
+ export { readJson, writeJson } from "./util/json.mjs";
48
+ export {
49
+ assertSafeId,
50
+ assertSafeSkillName,
51
+ assertSafeSkillPath,
52
+ assertSafeSkillRoot,
53
+ assertSafeRelativePath,
54
+ } from "./skills/ids.mjs";
55
+ export {
56
+ GLOBAL_TARGETS,
57
+ LEGACY_PROFILE_FILE,
58
+ PROJECT_CONFIG_FILE,
59
+ PROJECT_LOCK_FILE,
60
+ PROJECT_TARGETS,
61
+ catalogCacheRoot,
62
+ defaultCatalogFile,
63
+ globalConfigFile,
64
+ globalLockFile,
65
+ knownCatalogsFile,
66
+ stateRoot,
67
+ } from "./skills/paths.mjs";
68
+ export {
69
+ cloneHead,
70
+ cloneRevision,
71
+ currentRepositoryState,
72
+ deriveSourceId,
73
+ git,
74
+ normalizeRepositoryInput,
75
+ remoteHead,
76
+ repositoryIdentity,
77
+ run,
78
+ } from "./skills/git.mjs";
79
+ export {
80
+ catalogDisplayName,
81
+ ensureCatalog,
82
+ loadDefaultCatalogSpec,
83
+ loadKnownCatalogs,
84
+ parseCatalogSpec,
85
+ registerCatalog,
86
+ registerKnownCatalog,
87
+ setDefaultCatalogSpec,
88
+ } from "./skills/catalog.mjs";
89
+ export {
90
+ addDirectSkills,
91
+ directLicensesRoot,
92
+ directRoot,
93
+ readDirectState,
94
+ removeDirectSkills,
95
+ removeExternalSkills,
96
+ writeDirectState,
97
+ } from "./skills/direct.mjs";
98
+ export { printTree } from "./skills/ui.mjs";
99
+ export {
100
+ buildCatalog,
101
+ detectSkillRoot,
102
+ discoverSourceSkills,
103
+ findSource,
104
+ loadSources,
105
+ parseFrontmatterName,
106
+ readSkill,
107
+ registerSource,
108
+ saveSources,
109
+ stageSource,
110
+ } from "./skills/sources.mjs";
111
+ export {
112
+ addSkillsToPacks,
113
+ catalogReferences,
114
+ loadPacks,
115
+ normalizePackIds,
116
+ packContainsSkill,
117
+ parsePackArguments,
118
+ pruneCatalogSkills,
119
+ resolvePack,
120
+ resolvePacks,
121
+ skillCoveredByPacks,
122
+ } from "./skills/packs.mjs";
123
+ export {
124
+ createTempDirectory,
125
+ removeTempDirectory,
126
+ replaceStagedFiles,
127
+ } from "./skills/vendor.mjs";
128
+ export {
129
+ createInstallContext,
130
+ installCopies,
131
+ installPacks,
132
+ installedPackIds,
133
+ isCatalogDirectory,
134
+ previousManagedState,
135
+ removeAllManagedSkills,
136
+ removeInstallationFiles,
137
+ removeSkillDirectories,
138
+ resolveInstallPacks,
139
+ resolveInstallSource,
140
+ skillsInstallationStatus,
141
+ uninstallPacks,
142
+ writeInstallMetadata,
143
+ } from "./skills/install.mjs";
@@ -0,0 +1,81 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import path from "node:path";
4
+ import { runtimePaths } from "../config.mjs";
5
+ import {
6
+ PROJECT_ROOT_TOKEN,
7
+ listFiles,
8
+ mergeFiles,
9
+ revertFrom,
10
+ samePath,
11
+ snapshotFiles,
12
+ snapshotInto,
13
+ transformJsonLines,
14
+ } from "../sessions.mjs";
15
+
16
+ export function claudeProjectKey(projectRoot) {
17
+ return path.resolve(projectRoot).replace(/[^a-zA-Z0-9]/g, "-");
18
+ }
19
+
20
+ function locations(projectRoot, environment = process.env) {
21
+ const claudeHome = environment.CLAUDE_CONFIG_DIR || path.join(homedir(), ".claude");
22
+ return {
23
+ native: path.join(claudeHome, "projects", claudeProjectKey(projectRoot)),
24
+ portable: path.join(runtimePaths(projectRoot).sessionsRoot, "claude"),
25
+ };
26
+ }
27
+
28
+ function rewriteCwd(content, projectRoot, restore) {
29
+ return transformJsonLines(content.toString("utf8"), (record) => {
30
+ if (restore ? typeof record.cwd === "string" : samePath(record.cwd, projectRoot)) {
31
+ record.cwd = restore ? projectRoot : PROJECT_ROOT_TOKEN;
32
+ }
33
+ return record;
34
+ });
35
+ }
36
+
37
+ export async function capture(projectRoot, options = {}) {
38
+ const { native, portable } = locations(projectRoot, options.environment);
39
+ const files = await listFiles(native);
40
+ if (files.length === 0) {
41
+ return { count: 0, changed: false };
42
+ }
43
+ await snapshotFiles(native, files, portable, (content, relative) =>
44
+ relative.endsWith(".jsonl") ? rewriteCwd(content, projectRoot, false) : content,
45
+ );
46
+ return { count: files.filter((file) => file.endsWith(".jsonl") && !file.includes(`${path.sep}subagents${path.sep}`)).length, changed: true };
47
+ }
48
+
49
+ export async function restore(projectRoot, options = {}) {
50
+ const { native, portable } = locations(projectRoot, options.environment);
51
+ const files = await listFiles(portable);
52
+ if (files.length === 0) {
53
+ return { count: 0, added: 0, updated: 0, unchanged: 0 };
54
+ }
55
+ // Project portable sessions are the source of truth: on conflict they
56
+ // overwrite the native copy (explicit `sessions writeback` semantics).
57
+ const result = await mergeFiles(portable, files, native, (content, relative) =>
58
+ relative.endsWith(".jsonl") ? rewriteCwd(content, projectRoot, true) : content,
59
+ { onConflict: "keep-source" },
60
+ );
61
+ return { count: files.length, ...result };
62
+ }
63
+
64
+ export async function status(projectRoot) {
65
+ const { portable } = locations(projectRoot);
66
+ const files = await listFiles(portable);
67
+ return { count: files.filter((file) => file.endsWith(".jsonl") && !file.includes(`${path.sep}subagents${path.sep}`)).length };
68
+ }
69
+
70
+ // Save the native project directory into the shared launch state so the last
71
+ // exit can restore it: sessions created by `avenic claude` must live only
72
+ // in the project, never in the global native storage.
73
+ export async function snapshotNative(projectRoot, snapshotRoot, options = {}) {
74
+ const { native } = locations(projectRoot, options.environment);
75
+ await snapshotInto(native, snapshotRoot);
76
+ }
77
+
78
+ export async function revertNative(snapshotRoot, projectRoot, options = {}) {
79
+ const { native } = locations(projectRoot, options.environment);
80
+ await revertFrom(snapshotRoot, native);
81
+ }
@@ -0,0 +1,142 @@
1
+ import { existsSync } from "node:fs";
2
+ import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import path from "node:path";
5
+ import { runtimePaths } from "../config.mjs";
6
+ import {
7
+ PROJECT_ROOT_TOKEN,
8
+ listFiles,
9
+ mergeFiles,
10
+ readFirstJsonLine,
11
+ replaceDirectory,
12
+ revertFrom,
13
+ samePath,
14
+ snapshotInto,
15
+ transformJsonLines,
16
+ } from "../sessions.mjs";
17
+
18
+ function locations(projectRoot, environment = process.env) {
19
+ const codexHome = environment.CODEX_HOME || path.join(homedir(), ".codex");
20
+ return {
21
+ codexHome,
22
+ nativeSessions: path.join(codexHome, "sessions"),
23
+ portable: path.join(runtimePaths(projectRoot).sessionsRoot, "codex"),
24
+ };
25
+ }
26
+
27
+ function rewriteCwd(content, projectRoot, restore) {
28
+ return transformJsonLines(content.toString("utf8"), (record) => {
29
+ const cwd = record.payload?.cwd;
30
+ if (restore ? typeof cwd === "string" : samePath(cwd, projectRoot)) {
31
+ record.payload.cwd = restore ? projectRoot : PROJECT_ROOT_TOKEN;
32
+ }
33
+ return record;
34
+ });
35
+ }
36
+
37
+ async function matchingRollouts(root, projectRoot) {
38
+ const matches = [];
39
+ for (const relative of await listFiles(root)) {
40
+ if (!relative.endsWith(".jsonl")) {
41
+ continue;
42
+ }
43
+ try {
44
+ const first = await readFirstJsonLine(path.join(root, relative));
45
+ if (first.type === "session_meta" && samePath(first.payload?.cwd, projectRoot)) {
46
+ matches.push({ relative, id: first.payload?.id ?? first.payload?.session_id });
47
+ }
48
+ } catch {}
49
+ }
50
+ return matches;
51
+ }
52
+
53
+ async function filteredIndex(codexHome, ids) {
54
+ const indexFile = path.join(codexHome, "session_index.jsonl");
55
+ if (!existsSync(indexFile)) {
56
+ return "";
57
+ }
58
+ return (await readFile(indexFile, "utf8"))
59
+ .split(/\r?\n/)
60
+ .filter((line) => {
61
+ if (!line.trim()) return false;
62
+ try {
63
+ return ids.has(JSON.parse(line).id);
64
+ } catch {
65
+ return false;
66
+ }
67
+ })
68
+ .join("\n");
69
+ }
70
+
71
+ export async function capture(projectRoot, options = {}) {
72
+ const { codexHome, nativeSessions, portable } = locations(projectRoot, options.environment);
73
+ const rollouts = await matchingRollouts(nativeSessions, projectRoot);
74
+ if (rollouts.length === 0) {
75
+ return { count: 0, changed: false };
76
+ }
77
+ const ids = new Set(rollouts.map(({ id }) => id).filter(Boolean));
78
+ await replaceDirectory(portable, async (temporary) => {
79
+ for (const { relative } of rollouts) {
80
+ const target = path.join(temporary, "sessions", relative);
81
+ await mkdir(path.dirname(target), { recursive: true });
82
+ await writeFile(target, rewriteCwd(await readFile(path.join(nativeSessions, relative)), projectRoot, false));
83
+ }
84
+ const index = await filteredIndex(codexHome, ids);
85
+ if (index) {
86
+ await writeFile(path.join(temporary, "session_index.jsonl"), `${index}\n`, "utf8");
87
+ }
88
+ });
89
+ return { count: rollouts.length, changed: true };
90
+ }
91
+
92
+ async function restoreIndex(portable, codexHome) {
93
+ const source = path.join(portable, "session_index.jsonl");
94
+ if (!existsSync(source)) {
95
+ return;
96
+ }
97
+ const destination = path.join(codexHome, "session_index.jsonl");
98
+ const existing = existsSync(destination) ? await readFile(destination, "utf8") : "";
99
+ const existingLines = new Set(existing.split(/\r?\n/).filter(Boolean));
100
+ const additions = (await readFile(source, "utf8")).split(/\r?\n/).filter((line) => line && !existingLines.has(line));
101
+ if (additions.length > 0) {
102
+ await mkdir(codexHome, { recursive: true });
103
+ await appendFile(destination, `${existing && !existing.endsWith("\n") ? "\n" : ""}${additions.join("\n")}\n`, "utf8");
104
+ }
105
+ }
106
+
107
+ export async function restore(projectRoot, options = {}) {
108
+ const { codexHome, nativeSessions, portable } = locations(projectRoot, options.environment);
109
+ const sourceRoot = path.join(portable, "sessions");
110
+ const files = await listFiles(sourceRoot);
111
+ if (files.length === 0) {
112
+ return { count: 0, added: 0, updated: 0, unchanged: 0 };
113
+ }
114
+ // Project portable sessions are the source of truth: on conflict they
115
+ // overwrite the native copy (explicit `sessions writeback` semantics).
116
+ const result = await mergeFiles(sourceRoot, files, nativeSessions, (content, relative) =>
117
+ relative.endsWith(".jsonl") ? rewriteCwd(content, projectRoot, true) : content,
118
+ { onConflict: "keep-source" },
119
+ );
120
+ await restoreIndex(portable, codexHome);
121
+ return { count: files.length, ...result };
122
+ }
123
+
124
+ export async function status(projectRoot) {
125
+ const { portable } = locations(projectRoot);
126
+ return { count: (await listFiles(path.join(portable, "sessions"))).filter((file) => file.endsWith(".jsonl")).length };
127
+ }
128
+
129
+ // Save the native sessions directory and session index into the shared launch
130
+ // state so the last exit can restore them: sessions created by `avenic
131
+ // codex` must live only in the project, never in the global native storage.
132
+ export async function snapshotNative(projectRoot, snapshotRoot, options = {}) {
133
+ const { codexHome, nativeSessions } = locations(projectRoot, options.environment);
134
+ await snapshotInto(nativeSessions, path.join(snapshotRoot, "sessions"));
135
+ await snapshotInto(path.join(codexHome, "session_index.jsonl"), path.join(snapshotRoot, "index.jsonl"));
136
+ }
137
+
138
+ export async function revertNative(snapshotRoot, projectRoot, options = {}) {
139
+ const { codexHome, nativeSessions } = locations(projectRoot, options.environment);
140
+ await revertFrom(path.join(snapshotRoot, "index.jsonl"), path.join(codexHome, "session_index.jsonl"));
141
+ await revertFrom(path.join(snapshotRoot, "sessions"), nativeSessions);
142
+ }
@@ -0,0 +1,9 @@
1
+ import * as claude from "./claude.mjs";
2
+ import * as codex from "./codex.mjs";
3
+ import * as opencode from "./opencode.mjs";
4
+
5
+ const ADAPTERS = { claude, codex, opencode };
6
+
7
+ export function getSessionAdapter(agentId) {
8
+ return ADAPTERS[agentId];
9
+ }
@@ -0,0 +1,76 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { runtimePaths } from "../config.mjs";
5
+ import { hashContent, listFiles, replaceDirectory, samePath } from "../sessions.mjs";
6
+ import { spawnExecutableSync } from "../process.mjs";
7
+
8
+ function run(argumentsList, projectRoot, options = {}) {
9
+ const result = spawnExecutableSync("opencode", argumentsList, {
10
+ cwd: projectRoot,
11
+ encoding: "utf8",
12
+ env: options.environment ?? process.env,
13
+ windowsHide: true,
14
+ spawn: options.spawn,
15
+ });
16
+ if (result.error) {
17
+ throw new Error(`Unable to launch opencode: ${result.error.message}`);
18
+ }
19
+ if (result.status !== 0) {
20
+ throw new Error(result.stderr?.trim() || `opencode exited with code ${result.status}`);
21
+ }
22
+ return result.stdout;
23
+ }
24
+
25
+ function portableRoot(projectRoot) {
26
+ return path.join(runtimePaths(projectRoot).sessionsRoot, "opencode");
27
+ }
28
+
29
+ function matchingSessions(projectRoot, options) {
30
+ const sessions = JSON.parse(run(["session", "list", "--format", "json"], projectRoot, options));
31
+ return sessions.filter((session) => samePath(session.directory, projectRoot));
32
+ }
33
+
34
+ export async function capture(projectRoot, options = {}) {
35
+ const sessions = matchingSessions(projectRoot, options);
36
+ if (sessions.length === 0) {
37
+ return { count: 0, changed: false };
38
+ }
39
+ await replaceDirectory(portableRoot(projectRoot), async (temporary) => {
40
+ for (const session of sessions) {
41
+ await writeFile(path.join(temporary, `${session.id}.json`), run(["export", session.id], projectRoot, options), "utf8");
42
+ }
43
+ });
44
+ return { count: sessions.length, changed: true };
45
+ }
46
+
47
+ export async function restore(projectRoot, options = {}) {
48
+ const portable = portableRoot(projectRoot);
49
+ const files = (await listFiles(portable)).filter((file) => file.endsWith(".json"));
50
+ if (files.length === 0) {
51
+ return { count: 0, added: 0, updated: 0, unchanged: 0 };
52
+ }
53
+ const localRoot = path.join(runtimePaths(projectRoot).localRoot, "opencode");
54
+ const manifestFile = path.join(localRoot, "session-imports.json");
55
+ const manifest = existsSync(manifestFile) ? JSON.parse(await readFile(manifestFile, "utf8")) : {};
56
+ let imported = 0;
57
+ let unchanged = 0;
58
+ for (const relative of files) {
59
+ const file = path.join(portable, relative);
60
+ const hash = hashContent(await readFile(file));
61
+ if (manifest[relative] === hash) {
62
+ unchanged += 1;
63
+ continue;
64
+ }
65
+ run(["import", file], projectRoot, options);
66
+ manifest[relative] = hash;
67
+ imported += 1;
68
+ }
69
+ await mkdir(localRoot, { recursive: true });
70
+ await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
71
+ return { count: files.length, added: imported, updated: 0, unchanged };
72
+ }
73
+
74
+ export async function status(projectRoot) {
75
+ return { count: (await listFiles(portableRoot(projectRoot))).filter((file) => file.endsWith(".json")).length };
76
+ }
@@ -0,0 +1,39 @@
1
+ import process from "node:process";
2
+ import { spawnExecutableSync } from "./process.mjs";
3
+
4
+ export const AGENTS = {
5
+ claude: {
6
+ displayName: "Claude Code",
7
+ executable: "claude",
8
+ },
9
+ codex: {
10
+ displayName: "Codex",
11
+ executable: "codex",
12
+ },
13
+ opencode: {
14
+ displayName: "OpenCode",
15
+ executable: "opencode",
16
+ },
17
+ };
18
+
19
+ export function getAgent(agentId) {
20
+ const agent = AGENTS[agentId];
21
+ if (!agent) {
22
+ throw new Error(`Unknown Agent: ${agentId}`);
23
+ }
24
+ return { id: agentId, ...agent };
25
+ }
26
+
27
+ export function agentExecutableAvailable(agentId, environment = process.env) {
28
+ const agent = getAgent(agentId);
29
+ try {
30
+ const result = spawnExecutableSync(agent.executable, ["--version"], {
31
+ env: environment,
32
+ stdio: "pipe",
33
+ windowsHide: true,
34
+ });
35
+ return result.status === 0;
36
+ } catch {
37
+ return false;
38
+ }
39
+ }