@sayansr26/agent-os 0.5.3 → 0.6.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +59 -0
- package/README.md +27 -7
- package/package.json +1 -1
- package/plugins/agent-os/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-os/agents/architect.md +5 -0
- package/plugins/agent-os/agents/builder.md +7 -2
- package/plugins/agent-os/agents/documenter.md +6 -1
- package/plugins/agent-os/agents/feature-cartographer.md +18 -1
- package/plugins/agent-os/agents/orchestrator.md +5 -0
- package/plugins/agent-os/agents/reviewer.md +5 -0
- package/plugins/agent-os/agents/tester.md +5 -0
- package/plugins/agent-os/hooks/hooks.json +14 -1
- package/plugins/agent-os/hooks/pre-edit.mjs +83 -0
- package/plugins/agent-os/hooks/session-resume.mjs +75 -18
- package/plugins/agent-os/lib/features.mjs +154 -0
- package/plugins/agent-os/skills/init/SKILL.md +72 -32
- package/plugins/agent-os/skills/init/references/changing-a-feature.md +7 -0
- package/plugins/agent-os/skills/init/references/establishing.md +28 -0
- package/plugins/agent-os/skills/init/references/git-permissions.md +5 -2
- package/plugins/agent-os/skills/init/scripts/audit.mjs +73 -2
- package/plugins/agent-os/skills/init/scripts/settings.mjs +186 -0
- package/plugins/agent-os/skills/init/scripts/state.mjs +133 -0
- package/src/claude-setup.mjs +9 -78
- package/src/cli.mjs +71 -28
- package/src/plugin.mjs +74 -33
- package/src/selftest.mjs +62 -3
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-os settings — write the permission and task-tool setup, not describe it.
|
|
4
|
+
*
|
|
5
|
+
* Three things, at project scope, user scope, or both:
|
|
6
|
+
*
|
|
7
|
+
* settings.json permissions.deny += the git write-protection rules
|
|
8
|
+
* env.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1"
|
|
9
|
+
* CLAUDE.md the task-tracking rule
|
|
10
|
+
*
|
|
11
|
+
* Merges, never replaces: existing keys, allow rules, deny rules and a value
|
|
12
|
+
* the user set for the env flag all survive. Idempotent. Before changing a
|
|
13
|
+
* file under ~/.claude it copies it to `<file>.agent-os.bak`; project files
|
|
14
|
+
* are left to git, so a backup does not show up as an untracked file.
|
|
15
|
+
*
|
|
16
|
+
* Previews by default. `--apply` writes. The preview is what /agent-os:init
|
|
17
|
+
* shows the user before asking once; the CLI's `init` asks the same question.
|
|
18
|
+
*
|
|
19
|
+
* Usage: node settings.mjs [--root <dir>] [--scope project|user|both] [--apply]
|
|
20
|
+
*/
|
|
21
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
export const TODO_ENV = "CLAUDE_CODE_ENABLE_TODO_TOOLS";
|
|
27
|
+
|
|
28
|
+
export const TASK_RULE = `- **Always track work with the task tool (TaskCreate / TaskUpdate).** Any request
|
|
29
|
+
with more than one step gets a task list before work starts: one task per
|
|
30
|
+
deliverable, marked \`in_progress\` when started and \`completed\` only when
|
|
31
|
+
verified. Keep it current as scope changes, including work delegated to
|
|
32
|
+
subagents, so I can see what is done, running and left at any moment.`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every git command that changes the repository. Read-only inspection
|
|
36
|
+
* (status, log, diff, show, blame, …) stays available. The first five close
|
|
37
|
+
* the flag forms that would otherwise walk past a per-subcommand rule —
|
|
38
|
+
* references/git-permissions.md explains each judgment call.
|
|
39
|
+
*/
|
|
40
|
+
export const GIT_DENY = [
|
|
41
|
+
"Bash(git -C*)", "Bash(git -c*)", "Bash(git --git-dir*)", "Bash(git --work-tree*)", "Bash(git --exec-path*)",
|
|
42
|
+
"Bash(git add *)", "Bash(git am *)", "Bash(git apply *)", "Bash(git bisect *)", "Bash(git branch *)",
|
|
43
|
+
"Bash(git checkout *)", "Bash(git cherry-pick *)", "Bash(git clean *)", "Bash(git clone *)", "Bash(git commit *)",
|
|
44
|
+
"Bash(git config *)", "Bash(git fast-import *)", "Bash(git filter-branch *)", "Bash(git gc *)", "Bash(git init *)",
|
|
45
|
+
"Bash(git merge *)", "Bash(git mv *)", "Bash(git notes *)", "Bash(git prune *)", "Bash(git pull *)",
|
|
46
|
+
"Bash(git push *)", "Bash(git rebase *)", "Bash(git reflog *)", "Bash(git remote *)", "Bash(git repack *)",
|
|
47
|
+
"Bash(git replace *)", "Bash(git reset *)", "Bash(git restore *)", "Bash(git revert *)", "Bash(git rm *)",
|
|
48
|
+
"Bash(git stash *)", "Bash(git submodule *)", "Bash(git switch *)", "Bash(git symbolic-ref *)", "Bash(git tag *)",
|
|
49
|
+
"Bash(git update-ref *)", "Bash(git worktree *)",
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export const userDir = () => process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
53
|
+
|
|
54
|
+
export function paths(scope, root) {
|
|
55
|
+
return scope === "user"
|
|
56
|
+
? { settings: join(userDir(), "settings.json"), claudeMd: join(userDir(), "CLAUDE.md"), label: "USER (~/.claude)" }
|
|
57
|
+
: { settings: join(root, ".claude/settings.json"), claudeMd: join(root, "CLAUDE.md"), label: "PROJECT" };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const put = (p, text, keep) => {
|
|
61
|
+
if (keep && existsSync(p)) copyFileSync(p, `${p}.agent-os.bak`);
|
|
62
|
+
mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, text); };
|
|
63
|
+
|
|
64
|
+
// "Bash(git commit *)" and "Bash(git commit:*)" are the same rule to Claude Code.
|
|
65
|
+
const norm = (r) => r.replace(/:\*\)$/, " *)").replace(/\s+/g, " ");
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge the deny rules and the env flag into one settings.json.
|
|
69
|
+
* Returns { status: "changed"|"unchanged"|"invalid", file, denyAdded, env, current }
|
|
70
|
+
* where env is "added" | "present" | "conflict".
|
|
71
|
+
*/
|
|
72
|
+
export function mergeSettings(file, { dry = false, deny = GIT_DENY, todo = true, keep = false } = {}) {
|
|
73
|
+
let settings = {};
|
|
74
|
+
if (existsSync(file)) {
|
|
75
|
+
try { settings = JSON.parse(readFileSync(file, "utf8")); }
|
|
76
|
+
catch { return { status: "invalid", file, denyAdded: [], env: null }; }
|
|
77
|
+
}
|
|
78
|
+
const perms = settings.permissions || {};
|
|
79
|
+
const have = new Set((perms.deny || []).map(norm));
|
|
80
|
+
const denyAdded = deny.filter((r) => !have.has(norm(r)));
|
|
81
|
+
|
|
82
|
+
let env = null, current;
|
|
83
|
+
if (todo) {
|
|
84
|
+
current = (settings.env || {})[TODO_ENV];
|
|
85
|
+
// "true" and "1" are both truthy to Claude Code; an existing value is the
|
|
86
|
+
// user's choice and is left alone rather than normalised.
|
|
87
|
+
if (current === undefined) env = "added";
|
|
88
|
+
else env = current === "1" || current === "true" || current === true ? "present" : "conflict";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!denyAdded.length && env !== "added") return { status: "unchanged", file, denyAdded, env, current };
|
|
92
|
+
|
|
93
|
+
if (denyAdded.length) settings.permissions = { ...perms, deny: [...(perms.deny || []), ...denyAdded] };
|
|
94
|
+
if (env === "added") settings.env = { ...(settings.env || {}), [TODO_ENV]: "1" };
|
|
95
|
+
if (!dry) put(file, JSON.stringify(settings, null, 2) + "\n", keep);
|
|
96
|
+
return { status: "changed", file, denyAdded, env, current };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Put the task-tracking rule in a CLAUDE.md. Returns "added" | "present" | "no-file".
|
|
101
|
+
* A project CLAUDE.md is never created — that is the project's own
|
|
102
|
+
* always-loaded context, and a CLI inventing one leaves a stub nobody owns.
|
|
103
|
+
* The user's ~/.claude/CLAUDE.md is created when `create` is set.
|
|
104
|
+
*/
|
|
105
|
+
export function addTaskRule(file, { dry = false, create = false, keep = false } = {}) {
|
|
106
|
+
if (!existsSync(file)) {
|
|
107
|
+
if (!create) return { status: "no-file", file };
|
|
108
|
+
if (!dry) put(file, `# Personal working agreement\n\n${TASK_RULE}\n`);
|
|
109
|
+
return { status: "added", file, section: "new file" };
|
|
110
|
+
}
|
|
111
|
+
const text = readFileSync(file, "utf8");
|
|
112
|
+
if (/TaskCreate/.test(text)) return { status: "present", file };
|
|
113
|
+
|
|
114
|
+
// Prefer an operator-preferences section so the rule lands with the other
|
|
115
|
+
// instructions about how to work rather than orphaned at the bottom.
|
|
116
|
+
const heading = text.match(/^##\s+Operator preferences\s*$/m);
|
|
117
|
+
let next;
|
|
118
|
+
if (heading) {
|
|
119
|
+
const start = heading.index + heading[0].length;
|
|
120
|
+
const rest = text.slice(start);
|
|
121
|
+
const nextHeading = rest.search(/^##\s+/m);
|
|
122
|
+
const end = nextHeading === -1 ? text.length : start + nextHeading;
|
|
123
|
+
next = `${text.slice(0, end).replace(/\s*$/, "")}\n\n${TASK_RULE}\n\n${text.slice(end)}`;
|
|
124
|
+
} else {
|
|
125
|
+
next = `${text.replace(/\s*$/, "")}\n\n## Working agreement\n\n${TASK_RULE}\n`;
|
|
126
|
+
}
|
|
127
|
+
if (!dry) put(file, next.replace(/\n{4,}/g, "\n\n\n"), keep);
|
|
128
|
+
return { status: "added", file, section: heading ? "Operator preferences" : "Working agreement" };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Everything for one scope. */
|
|
132
|
+
export function applyScope(scope, root, { dry = false } = {}) {
|
|
133
|
+
const p = paths(scope, root);
|
|
134
|
+
return {
|
|
135
|
+
scope,
|
|
136
|
+
label: p.label,
|
|
137
|
+
settings: mergeSettings(p.settings, { dry, keep: scope === "user" }),
|
|
138
|
+
rule: addTaskRule(p.claudeMd, { dry, create: scope === "user", keep: scope === "user" }),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function describe(r, { dry }) {
|
|
143
|
+
const s = r.settings, lines = [];
|
|
144
|
+
const verb = dry ? "would add" : "added";
|
|
145
|
+
lines.push(`${r.label} ${s.file}`);
|
|
146
|
+
if (s.status === "invalid") lines.push(" not valid JSON — left alone; fix it and re-run");
|
|
147
|
+
else {
|
|
148
|
+
lines.push(s.denyAdded.length
|
|
149
|
+
? ` permissions.deny ${verb} ${s.denyAdded.length} git write rule(s)${s.denyAdded.length < GIT_DENY.length ? ` (${GIT_DENY.length - s.denyAdded.length} already there)` : ""}`
|
|
150
|
+
: ` permissions.deny git write protection already complete`);
|
|
151
|
+
lines.push({
|
|
152
|
+
added: ` env.${TODO_ENV} ${verb} "1"`,
|
|
153
|
+
present: ` env.${TODO_ENV} already on`,
|
|
154
|
+
conflict: ` env.${TODO_ENV} is "${s.current}" — left as set`,
|
|
155
|
+
}[s.env]);
|
|
156
|
+
if (s.status === "changed" && !dry && r.scope === "user" && existsSync(`${s.file}.agent-os.bak`)) lines.push(` backup ${s.file}.agent-os.bak`);
|
|
157
|
+
}
|
|
158
|
+
lines.push({
|
|
159
|
+
added: ` ${r.rule.file} ${dry ? "would add" : "added"} the task-tracking rule (${r.rule.section})`,
|
|
160
|
+
present: ` ${r.rule.file} task-tracking rule already there`,
|
|
161
|
+
"no-file": ` ${r.rule.file} absent — /agent-os:init builds it`,
|
|
162
|
+
}[r.rule.status]);
|
|
163
|
+
return lines;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function main(argv) {
|
|
167
|
+
const arg = (k, d) => { const i = argv.indexOf(k); return i === -1 ? d : argv[i + 1]; };
|
|
168
|
+
const root = arg("--root", process.cwd());
|
|
169
|
+
const scope = arg("--scope", "both");
|
|
170
|
+
const dry = !argv.includes("--apply");
|
|
171
|
+
const scopes = scope === "both" ? ["project", "user"] : [scope];
|
|
172
|
+
if (!scopes.every((s) => s === "project" || s === "user")) {
|
|
173
|
+
console.error(`--scope must be project, user or both (got "${scope}")`);
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
const out = [`AGENT-OS SETTINGS ${dry ? "preview — nothing written; re-run with --apply" : "applied"}`, ""];
|
|
177
|
+
for (const s of scopes) out.push(...describe(applyScope(s, root, { dry }), { dry }), "");
|
|
178
|
+
if (scopes.includes("user")) out.push("~/.claude applies to every project on this machine.");
|
|
179
|
+
process.stdout.write(out.join("\n") + "\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const isMain = () => {
|
|
183
|
+
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
|
|
184
|
+
catch { return false; }
|
|
185
|
+
};
|
|
186
|
+
if (process.argv[1] && isMain()) main(process.argv.slice(2));
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* agent-os setup state — is this a fresh setup, a repair, or already healthy?
|
|
4
|
+
*
|
|
5
|
+
* Both `npx agent-os init` and `/agent-os:init` start here, so neither
|
|
6
|
+
* re-scaffolds a project that is set up, and both name exactly what a repair
|
|
7
|
+
* has to touch. Read-only: every check is a file read, and the settings checks
|
|
8
|
+
* are dry runs of what settings.mjs would write.
|
|
9
|
+
*
|
|
10
|
+
* The plugin check reads Claude Code's own records (installed_plugins.json,
|
|
11
|
+
* enabledPlugins, the marketplace cache) instead of calling the `claude` CLI,
|
|
12
|
+
* so it also works from inside a session.
|
|
13
|
+
*
|
|
14
|
+
* Usage: node state.mjs [projectDir]
|
|
15
|
+
*/
|
|
16
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { fileURLToPath } from "node:url";
|
|
19
|
+
import { addTaskRule, GIT_DENY, mergeSettings, paths, userDir } from "./settings.mjs";
|
|
20
|
+
|
|
21
|
+
export const PLUGIN_ID = "agent-os@sayan-plugins";
|
|
22
|
+
export const MARKETPLACE_NAME = "sayan-plugins";
|
|
23
|
+
|
|
24
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
26
|
+
const real = (p) => { try { return realpathSync(p); } catch { return p; } };
|
|
27
|
+
|
|
28
|
+
/** -1, 0, 1 for dotted numeric versions; unknown sorts lowest. */
|
|
29
|
+
export function cmpVersion(a, b) {
|
|
30
|
+
const pa = String(a || "0").split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
31
|
+
const pb = String(b || "0").split(/[.-]/).map((n) => parseInt(n, 10) || 0);
|
|
32
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
33
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
34
|
+
if (d) return d > 0 ? 1 : -1;
|
|
35
|
+
}
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The version of the plugin this script ships in. */
|
|
40
|
+
export const bundledVersion = () => readJson(join(HERE, "../../../.claude-plugin/plugin.json"))?.version || null;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* What Claude Code has installed for this project.
|
|
44
|
+
* { installed, scope, version, enabled, latest } — latest is the version in the
|
|
45
|
+
* local marketplace cache, which is only as new as the last marketplace update.
|
|
46
|
+
*/
|
|
47
|
+
export function pluginState(root) {
|
|
48
|
+
const home = userDir();
|
|
49
|
+
const rootReal = real(root);
|
|
50
|
+
const records = readJson(join(home, "plugins/installed_plugins.json"))?.plugins?.[PLUGIN_ID] || [];
|
|
51
|
+
// Project and local installs belong to one directory; user installs to all.
|
|
52
|
+
const mine = records.filter((r) => r.scope === "user" || (r.projectPath && real(r.projectPath) === rootReal));
|
|
53
|
+
const pick = mine.find((r) => r.scope === "project" || r.scope === "local") || mine[0] || null;
|
|
54
|
+
|
|
55
|
+
const enabledIn = [
|
|
56
|
+
join(root, ".claude/settings.local.json"),
|
|
57
|
+
join(root, ".claude/settings.json"),
|
|
58
|
+
join(home, "settings.json"),
|
|
59
|
+
].map((p) => readJson(p)?.enabledPlugins?.[PLUGIN_ID]).find((v) => v !== undefined);
|
|
60
|
+
|
|
61
|
+
const latest = readJson(join(home, "plugins/marketplaces", MARKETPLACE_NAME, "plugins/agent-os/.claude-plugin/plugin.json"))?.version || null;
|
|
62
|
+
return {
|
|
63
|
+
installed: !!pick,
|
|
64
|
+
scope: pick?.scope || null,
|
|
65
|
+
version: pick?.version || null,
|
|
66
|
+
enabled: pick ? enabledIn !== false : false,
|
|
67
|
+
latest,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Every check, then the verdict:
|
|
73
|
+
* fresh nothing of agent-os is here yet — run the whole setup
|
|
74
|
+
* repair some of it is — fix only the items marked missing
|
|
75
|
+
* healthy all of it is
|
|
76
|
+
* `expect` is the plugin version the caller wants at minimum (the CLI passes
|
|
77
|
+
* the version it bundles).
|
|
78
|
+
*/
|
|
79
|
+
export function setupState(root, { expect = bundledVersion() } = {}) {
|
|
80
|
+
const items = [];
|
|
81
|
+
const add = (key, label, ok, fix) => items.push({ key, label, ok, fix });
|
|
82
|
+
|
|
83
|
+
const hasSource = existsSync(join(root, ".agent-os/config.json"));
|
|
84
|
+
const claudeMd = (() => { try { return readFileSync(join(root, "CLAUDE.md"), "utf8"); } catch { return null; } })();
|
|
85
|
+
const hasMemory = existsSync(join(root, ".claude/agent-memory"));
|
|
86
|
+
|
|
87
|
+
add("source", ".agent-os/ source", hasSource, "npx @sayansr26/agent-os init");
|
|
88
|
+
add("claude-md", "CLAUDE.md", claudeMd !== null, "/init in Claude Code, then /agent-os:init");
|
|
89
|
+
|
|
90
|
+
for (const scope of ["project", "user"]) {
|
|
91
|
+
const p = paths(scope, root);
|
|
92
|
+
const s = mergeSettings(p.settings, { dry: true });
|
|
93
|
+
const where = scope === "user" ? "~/.claude/settings.json" : ".claude/settings.json";
|
|
94
|
+
if (s.status === "invalid") { add(`${scope}-settings`, `${where} is valid JSON`, false, `fix ${where} by hand`); continue; }
|
|
95
|
+
const have = GIT_DENY.length - s.denyAdded.length;
|
|
96
|
+
add(`${scope}-git`, `${where} git write protection (${have}/${GIT_DENY.length})`, !s.denyAdded.length, `agent-os settings --scope ${scope} --apply`);
|
|
97
|
+
add(`${scope}-todo`, `${where} task tools`, s.env !== "added", `agent-os settings --scope ${scope} --apply`);
|
|
98
|
+
}
|
|
99
|
+
if (claudeMd !== null) {
|
|
100
|
+
add("task-rule", "CLAUDE.md task-tracking rule", addTaskRule(join(root, "CLAUDE.md"), { dry: true }).status === "present", "agent-os settings --scope project --apply");
|
|
101
|
+
add("agents-section", "CLAUDE.md says when to use each agent", /agent-os:(feature-cartographer|builder|reviewer)/.test(claudeMd), "/agent-os:init (establishing.md, Step 4b)");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const pl = pluginState(root);
|
|
105
|
+
const want = [expect, pl.latest].filter(Boolean).sort((a, b) => cmpVersion(b, a))[0] || null;
|
|
106
|
+
add("plugin", "Claude Code plugin installed", pl.installed, "npx @sayansr26/agent-os init");
|
|
107
|
+
if (pl.installed) {
|
|
108
|
+
add("plugin-enabled", "plugin enabled", pl.enabled, `claude plugin enable ${PLUGIN_ID}`);
|
|
109
|
+
add("plugin-version", `plugin version ${pl.version}${want && cmpVersion(pl.version, want) < 0 ? ` (latest ${want})` : ""}`,
|
|
110
|
+
!want || cmpVersion(pl.version, want) >= 0, "npx @sayansr26/agent-os init (or /plugin update, then /reload-plugins)");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Settings alone do not count: deny rules may be the user's own.
|
|
114
|
+
const anything = hasSource || hasMemory || pl.installed || /agent-os/.test(claudeMd || "");
|
|
115
|
+
const missing = items.filter((i) => !i.ok);
|
|
116
|
+
const status = !anything ? "fresh" : missing.length ? "repair" : "healthy";
|
|
117
|
+
return { status, items, missing, plugin: pl };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function describeState(st) {
|
|
121
|
+
const lines = [`SETUP ${st.status.toUpperCase()}${st.status === "repair" ? ` — ${st.missing.length} item(s) to fix` : ""}`];
|
|
122
|
+
for (const i of st.items) lines.push(` ${i.ok ? "ok " : "MISSING"} ${i.label}${i.ok ? "" : ` → ${i.fix}`}`);
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const isMain = () => {
|
|
127
|
+
try { return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url)); }
|
|
128
|
+
catch { return false; }
|
|
129
|
+
};
|
|
130
|
+
if (process.argv[1] && isMain()) {
|
|
131
|
+
const root = process.argv[2] || process.cwd();
|
|
132
|
+
process.stdout.write(describeState(setupState(root)).join("\n") + "\n");
|
|
133
|
+
}
|
package/src/claude-setup.mjs
CHANGED
|
@@ -1,81 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The Claude Code settings that are not rules and not the plugin: git write
|
|
3
|
+
* protection, the todo tools flag, and the task-tracking rule.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* that the setup travels with the repository.
|
|
5
|
+
* The logic lives in the plugin (`skills/init/scripts/settings.mjs`) so that
|
|
6
|
+
* `/agent-os:init` inside Claude Code and `agent-os init` in a terminal write
|
|
7
|
+
* exactly the same thing. This module is the CLI's door to it.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const TODO_ENV = "CLAUDE_CODE_ENABLE_TODO_TOOLS";
|
|
14
|
-
|
|
15
|
-
export const TASK_RULE = `- **Always track work with the task tool (TaskCreate / TaskUpdate).** Any request
|
|
16
|
-
with more than one step gets a task list before work starts: one task per
|
|
17
|
-
deliverable, marked \`in_progress\` when started and \`completed\` only when
|
|
18
|
-
verified. Keep it current as scope changes, including work delegated to
|
|
19
|
-
subagents, so I can see what is done, running and left at any moment.`;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Turn on the todo tools in project settings, preserving every other key.
|
|
23
|
-
* Returns "added" | "present" | "conflict" | "invalid".
|
|
24
|
-
*/
|
|
25
|
-
export function ensureTodoEnv(root, { dry = false } = {}) {
|
|
26
|
-
const rel = ".claude/settings.json";
|
|
27
|
-
const p = join(root, rel);
|
|
28
|
-
let settings = {};
|
|
29
|
-
if (existsSync(p)) {
|
|
30
|
-
try { settings = JSON.parse(readFileSync(p, "utf8")); }
|
|
31
|
-
catch { return { status: "invalid", rel }; }
|
|
32
|
-
}
|
|
33
|
-
const env = settings.env || {};
|
|
34
|
-
const current = env[TODO_ENV];
|
|
35
|
-
|
|
36
|
-
// "true" and "1" are both truthy to Claude Code; an existing truthy value is
|
|
37
|
-
// the user's choice and is left alone rather than normalised.
|
|
38
|
-
if (current !== undefined) {
|
|
39
|
-
const on = current === "1" || current === "true" || current === true;
|
|
40
|
-
return { status: on ? "present" : "conflict", rel, current };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
settings.env = { ...env, [TODO_ENV]: "1" };
|
|
44
|
-
if (!dry) write(root, rel, JSON.stringify(settings, null, 2) + "\n");
|
|
45
|
-
return { status: "added", rel };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Put the task-tracking rule in CLAUDE.md if it is not already there.
|
|
50
|
-
* Never creates CLAUDE.md — that file is the project's own always-loaded
|
|
51
|
-
* context, and inventing one from a CLI is how a project ends up with a stub
|
|
52
|
-
* nobody owns. Returns "added" | "present" | "no-file".
|
|
53
|
-
*/
|
|
54
|
-
export function ensureTaskRule(root, { dry = false } = {}) {
|
|
55
|
-
const rel = "CLAUDE.md";
|
|
56
|
-
const p = join(root, rel);
|
|
57
|
-
if (!existsSync(p)) return { status: "no-file", rel };
|
|
58
|
-
|
|
59
|
-
const text = readFileSync(p, "utf8");
|
|
60
|
-
if (/TaskCreate/.test(text)) return { status: "present", rel };
|
|
61
|
-
|
|
62
|
-
// Prefer the operator-preferences section if the project keeps one, so the
|
|
63
|
-
// rule lands with the other instructions about how to work rather than
|
|
64
|
-
// orphaned at the bottom of the file.
|
|
65
|
-
const heading = text.match(/^##\s+Operator preferences\s*$/m);
|
|
66
|
-
let next;
|
|
67
|
-
if (heading) {
|
|
68
|
-
const start = heading.index + heading[0].length;
|
|
69
|
-
const rest = text.slice(start);
|
|
70
|
-
const nextHeading = rest.search(/^##\s+/m);
|
|
71
|
-
const end = nextHeading === -1 ? text.length : start + nextHeading;
|
|
72
|
-
const before = text.slice(0, end).replace(/\s*$/, "");
|
|
73
|
-
next = `${before}\n\n${TASK_RULE}\n\n${text.slice(end)}`;
|
|
74
|
-
} else {
|
|
75
|
-
next = `${text.replace(/\s*$/, "")}\n\n## Working agreement\n\n${TASK_RULE}\n`;
|
|
76
|
-
}
|
|
77
|
-
next = next.replace(/\n{4,}/g, "\n\n\n");
|
|
78
|
-
|
|
79
|
-
if (!dry) write(root, rel, next);
|
|
80
|
-
return { status: "added", rel, section: heading ? "Operator preferences" : "Working agreement" };
|
|
81
|
-
}
|
|
9
|
+
export {
|
|
10
|
+
TODO_ENV, TASK_RULE, GIT_DENY, userDir, paths,
|
|
11
|
+
mergeSettings, addTaskRule, applyScope, describe,
|
|
12
|
+
} from "../plugins/agent-os/skills/init/scripts/settings.mjs";
|
package/src/cli.mjs
CHANGED
|
@@ -5,8 +5,10 @@ import { join } from "node:path";
|
|
|
5
5
|
import { detect, summarise, TOOLS } from "./detect.mjs";
|
|
6
6
|
import { load, write, readIfExists, matches, BANNER, DIR } from "./source.mjs";
|
|
7
7
|
import { adopt } from "./adopt.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { ensurePlugin, MARKETPLACE_NAME, PLUGIN } from "./plugin.mjs";
|
|
9
|
+
import { setupState, describeState } from "../plugins/agent-os/skills/init/scripts/state.mjs";
|
|
10
|
+
import { applyScope, describe } from "./claude-setup.mjs";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
10
12
|
import { compile, TARGETS } from "./targets.mjs";
|
|
11
13
|
|
|
12
14
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
@@ -15,12 +17,14 @@ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
|
15
17
|
const USAGE = `
|
|
16
18
|
${bold("agent-os")} — one source of truth for AI coding agent config
|
|
17
19
|
|
|
18
|
-
agent-os init
|
|
20
|
+
agent-os init set up a fresh project, or repair and update an existing one
|
|
19
21
|
agent-os sync recompile after editing .agent-os/
|
|
20
22
|
agent-os check verify nothing drifted (exit 1 if it has) — for CI
|
|
21
23
|
agent-os detect list which tools this project is set up for
|
|
22
24
|
agent-os audit inspect the project's context layer and report findings
|
|
23
25
|
agent-os memory inspect and health-check every memory store
|
|
26
|
+
agent-os settings preview git write protection + task tools for Claude Code
|
|
27
|
+
(--scope project|user|both, --apply to write)
|
|
24
28
|
|
|
25
29
|
Without a global install, prefix any of these with
|
|
26
30
|
${dim("npx @sayansr26/agent-os")}
|
|
@@ -29,7 +33,9 @@ Options
|
|
|
29
33
|
--root <dir> project directory (default: cwd)
|
|
30
34
|
--dry-run print what would change, write nothing
|
|
31
35
|
--force overwrite files agent-os did not generate (it refuses by default)
|
|
32
|
-
--no-plugin skip installing the Claude Code plugin during init
|
|
36
|
+
--no-plugin skip installing or updating the Claude Code plugin during init
|
|
37
|
+
--global init: also write ~/.claude settings without asking
|
|
38
|
+
--no-global init: leave ~/.claude alone
|
|
33
39
|
`;
|
|
34
40
|
|
|
35
41
|
function scaffold(root, found) {
|
|
@@ -116,6 +122,13 @@ export async function main(argv) {
|
|
|
116
122
|
return;
|
|
117
123
|
}
|
|
118
124
|
|
|
125
|
+
if (cmd === "settings") {
|
|
126
|
+
const i = argv.indexOf("--scope");
|
|
127
|
+
runScript("plugins/agent-os/skills/init/scripts/settings.mjs",
|
|
128
|
+
["--root", root, "--scope", i === -1 ? "both" : argv[i + 1], ...(argv.includes("--apply") ? ["--apply"] : [])]);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
119
132
|
if (cmd === "memory") {
|
|
120
133
|
runScript("plugins/agent-os/skills/memory/scripts/memory.mjs", argv.includes("--stale") ? [root, "--stale"] : [root]);
|
|
121
134
|
return;
|
|
@@ -123,14 +136,28 @@ export async function main(argv) {
|
|
|
123
136
|
|
|
124
137
|
let adopted = new Set();
|
|
125
138
|
|
|
139
|
+
let before = null;
|
|
126
140
|
if (cmd === "init") {
|
|
127
141
|
console.log(`\n${bold("agent-os init")} ${root}\n`);
|
|
142
|
+
// Decide fresh vs repair before touching anything, and say which, so a
|
|
143
|
+
// second run on a set-up project reads as a repair rather than a re-init.
|
|
144
|
+
before = setupState(root);
|
|
145
|
+
for (const l of describeState(before)) console.log(` ${l}`);
|
|
146
|
+
console.log({
|
|
147
|
+
fresh: `\n ${dim("Fresh setup: creating everything.")}`,
|
|
148
|
+
repair: `\n ${dim("Existing setup: fixing the items marked MISSING, leaving the rest as it is.")}`,
|
|
149
|
+
healthy: `\n ${dim("Setup is complete: re-syncing and checking the plugin for updates.")}`,
|
|
150
|
+
}[before.status]);
|
|
151
|
+
console.log("");
|
|
128
152
|
console.log(summarise(found));
|
|
153
|
+
const hadSource = existsSync(join(root, DIR));
|
|
129
154
|
const { present, taken } = scaffold(root, found);
|
|
130
155
|
adopted = taken.paths;
|
|
131
156
|
if (taken.from.length) {
|
|
132
157
|
console.log(`\n adopted into ${DIR}/ ${dim("— your existing files are now the source")}`);
|
|
133
158
|
for (const f of taken.from) console.log(` ${f}`);
|
|
159
|
+
} else if (hadSource) {
|
|
160
|
+
console.log(`\n kept ${DIR}/ ${dim("— already the source; nothing re-scaffolded")}`);
|
|
134
161
|
} else {
|
|
135
162
|
console.log(`\n created ${DIR}/ ${dim("(config.json, AGENTS.md, rules/example.md)")}`);
|
|
136
163
|
}
|
|
@@ -214,37 +241,53 @@ Pick one:
|
|
|
214
241
|
if (cmd === "init" && targets.includes("claude-code")) {
|
|
215
242
|
console.log(`\n${bold("Claude Code setup")}\n`);
|
|
216
243
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
244
|
+
// Project scope always: it travels with the repo. User scope protects
|
|
245
|
+
// every other project on the machine too, so it is asked once rather
|
|
246
|
+
// than assumed — unless --global / --no-global already answered.
|
|
247
|
+
for (const l of describe(applyScope("project", root, { dry }), { dry })) console.log(` ${l}`);
|
|
248
|
+
|
|
249
|
+
let global = argv.includes("--global") ? true : argv.includes("--no-global") ? false : null;
|
|
250
|
+
if (global === null && !dry && process.stdin.isTTY && process.stdout.isTTY) {
|
|
251
|
+
console.log("");
|
|
252
|
+
for (const l of describe(applyScope("user", root, { dry: true }), { dry: true })) console.log(` ${l}`);
|
|
253
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
254
|
+
const a = (await rl.question(`\n Apply the same to ~/.claude (every project on this machine)? [Y/n] `)).trim().toLowerCase();
|
|
255
|
+
rl.close();
|
|
256
|
+
global = a === "" || a === "y" || a === "yes";
|
|
257
|
+
}
|
|
258
|
+
if (global) {
|
|
259
|
+
console.log("");
|
|
260
|
+
for (const l of describe(applyScope("user", root, { dry }), { dry })) console.log(` ${l}`);
|
|
261
|
+
} else if (global === null) {
|
|
262
|
+
console.log(`\n ${dim("~/.claude left alone (not a terminal). To protect every project:")}`);
|
|
263
|
+
console.log(` ${dim("npx @sayansr26/agent-os settings --scope user --apply")}`);
|
|
264
|
+
}
|
|
233
265
|
}
|
|
234
266
|
|
|
235
267
|
if (cmd === "init" && targets.includes("claude-code") && !argv.includes("--no-plugin")) {
|
|
236
268
|
console.log(`\n${bold("Claude Code plugin")} ${dim(`${PLUGIN}@${MARKETPLACE_NAME}`)}\n`);
|
|
237
|
-
const r =
|
|
269
|
+
const r = ensurePlugin({ root, dry });
|
|
238
270
|
for (const line of r.done) console.log(` ${dim(line)}`);
|
|
239
|
-
if (r.ok
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
271
|
+
if (r.ok) console.log({
|
|
272
|
+
installed: `\n installed ${r.to || ""} at project scope ${dim("— .claude/settings.json, so it travels with the repo")}`,
|
|
273
|
+
updated: `\n updated ${r.from} → ${r.to}`,
|
|
274
|
+
current: `\n already the latest (${r.to})`,
|
|
275
|
+
"dry-run": `\n ${dim(r.from ? `installed: ${r.from} — would refresh and update` : "not installed — would install")}`,
|
|
276
|
+
}[r.reason]);
|
|
277
|
+
for (const line of r.hint) console.log(` ${r.ok ? dim(line) : line}`);
|
|
278
|
+
if (r.reason === "installed" || r.reason === "updated")
|
|
279
|
+
console.log(` ${dim("restart Claude Code, or /reload-plugins, to load it")}`);
|
|
246
280
|
console.log(`\n ${dim("--no-plugin skips this")}`);
|
|
247
281
|
}
|
|
282
|
+
|
|
283
|
+
if (cmd === "init" && !dry) {
|
|
284
|
+
const after = setupState(root);
|
|
285
|
+
console.log(`\n${bold("Result")} ${before.status} → ${after.status}`);
|
|
286
|
+
if (after.missing.length) {
|
|
287
|
+
console.log(dim(" still to do:"));
|
|
288
|
+
for (const i of after.missing) console.log(` ${i.label} → ${i.fix}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
248
291
|
console.log("");
|
|
249
292
|
return;
|
|
250
293
|
}
|