@coworker-jp/aidr 0.0.1
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/README.md +55 -0
- package/bin/aidr.js +2 -0
- package/package.json +29 -0
- package/src/agents/_stub.mjs +18 -0
- package/src/agents/aider.mjs +5 -0
- package/src/agents/amazonq.mjs +5 -0
- package/src/agents/amp.mjs +5 -0
- package/src/agents/antigravity.mjs +5 -0
- package/src/agents/claude.mjs +92 -0
- package/src/agents/cline.mjs +5 -0
- package/src/agents/codex.mjs +225 -0
- package/src/agents/continue.mjs +5 -0
- package/src/agents/copilot.mjs +5 -0
- package/src/agents/crush.mjs +5 -0
- package/src/agents/cursor.mjs +109 -0
- package/src/agents/gemini.mjs +66 -0
- package/src/agents/index.mjs +36 -0
- package/src/agents/jetbrains.mjs +5 -0
- package/src/agents/kiro.mjs +79 -0
- package/src/agents/opencode.mjs +5 -0
- package/src/agents/qwen.mjs +5 -0
- package/src/agents/roo.mjs +5 -0
- package/src/agents/standalone.mjs +43 -0
- package/src/agents/trae.mjs +5 -0
- package/src/agents/windsurf.mjs +105 -0
- package/src/binary-fetcher.mjs +157 -0
- package/src/browser-extension.mjs +83 -0
- package/src/cli.mjs +494 -0
- package/src/detect.mjs +39 -0
- package/src/fs-utils.mjs +247 -0
- package/src/merge.mjs +412 -0
- package/src/scheduled.mjs +219 -0
- package/src/templates.mjs +759 -0
- package/src/toml-merge.mjs +167 -0
- package/src/verify.mjs +51 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Line-based TOML merger for Codex's ~/.codex/config.toml feature flag.
|
|
2
|
+
// Codex requires `[features] codex_hooks = true` to activate hook dispatch.
|
|
3
|
+
// Users may already have their own config (model settings, other features), so
|
|
4
|
+
// we must merge rather than overwrite.
|
|
5
|
+
//
|
|
6
|
+
// Scope: *only* handles a single key inside a single section. No full TOML
|
|
7
|
+
// parser, no external deps. Preserves comments, blank lines, ordering, and
|
|
8
|
+
// other sections exactly.
|
|
9
|
+
|
|
10
|
+
const SECTION = "features";
|
|
11
|
+
const KEY = "codex_hooks";
|
|
12
|
+
const VALUE = "true";
|
|
13
|
+
|
|
14
|
+
// Match a section header line: optional leading whitespace, `[name]`, optional trailing comment.
|
|
15
|
+
function sectionHeader(name) {
|
|
16
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17
|
+
return new RegExp(`^\\s*\\[\\s*${escaped}\\s*\\](?:\\s*#.*)?$`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Match any section header (used to find section boundaries).
|
|
21
|
+
const ANY_SECTION_RE = /^\s*\[\s*[^\]]+\s*\](?:\s*#.*)?$/;
|
|
22
|
+
|
|
23
|
+
// Match `key = value` with optional trailing comment; capture the value token.
|
|
24
|
+
function keyAssignment(key) {
|
|
25
|
+
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
return new RegExp(`^(\\s*${escaped}\\s*=\\s*)([^#\\n]*?)(\\s*(?:#.*)?)$`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Detect obvious malformed TOML so we fail closed instead of silently corrupting.
|
|
30
|
+
// Only catches the most common case (unclosed bracket on a section header line) —
|
|
31
|
+
// full TOML validation is out of scope.
|
|
32
|
+
function validate(text) {
|
|
33
|
+
const lines = text.split("\n");
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
// Strip inline string content before looking for unbalanced brackets.
|
|
37
|
+
const stripped = line.replace(/"[^"]*"/g, "").replace(/'[^']*'/g, "");
|
|
38
|
+
// A line that starts with `[` must contain a closing `]` before any `#`.
|
|
39
|
+
if (/^\s*\[/.test(stripped)) {
|
|
40
|
+
const preComment = stripped.split("#")[0];
|
|
41
|
+
const opens = (preComment.match(/\[/g) || []).length;
|
|
42
|
+
const closes = (preComment.match(/\]/g) || []).length;
|
|
43
|
+
if (opens !== closes) {
|
|
44
|
+
throw new Error(`malformed TOML at line ${i + 1}: unbalanced brackets (${line.trim()})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function findSectionRange(lines, section) {
|
|
51
|
+
const headerRe = sectionHeader(section);
|
|
52
|
+
let start = -1;
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
if (headerRe.test(lines[i])) { start = i; break; }
|
|
55
|
+
}
|
|
56
|
+
if (start === -1) return null;
|
|
57
|
+
let end = lines.length;
|
|
58
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
59
|
+
if (ANY_SECTION_RE.test(lines[i])) { end = i; break; }
|
|
60
|
+
}
|
|
61
|
+
return { start, end };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Merge `[features] codex_hooks = true` into the given TOML text, preserving
|
|
65
|
+
// everything else. Idempotent: re-running on an already-merged file is a no-op.
|
|
66
|
+
export function mergeCodexHookFeature(text) {
|
|
67
|
+
const input = text ?? "";
|
|
68
|
+
if (input !== "") validate(input);
|
|
69
|
+
|
|
70
|
+
// Empty file / whitespace-only: write a fresh minimal TOML.
|
|
71
|
+
if (input.trim() === "") {
|
|
72
|
+
return `[${SECTION}]\n${KEY} = ${VALUE}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lines = input.split("\n");
|
|
76
|
+
// Preserve final newline state — JSON.stringify-style: if original ended with
|
|
77
|
+
// "\n", .split gives a trailing "" element we'll keep and rejoin with.
|
|
78
|
+
const range = findSectionRange(lines, SECTION);
|
|
79
|
+
const assignRe = keyAssignment(KEY);
|
|
80
|
+
|
|
81
|
+
if (range) {
|
|
82
|
+
// Section exists — look for the key inside it.
|
|
83
|
+
for (let i = range.start + 1; i < range.end; i++) {
|
|
84
|
+
const m = assignRe.exec(lines[i]);
|
|
85
|
+
if (m) {
|
|
86
|
+
const currentValue = m[2].trim();
|
|
87
|
+
if (currentValue === VALUE) return input; // already correct, no-op
|
|
88
|
+
lines[i] = `${m[1]}${VALUE}${m[3]}`; // overwrite `false` / other
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Key absent — insert right after the section header (before any subkeys),
|
|
93
|
+
// so the new line doesn't get buried under a long section.
|
|
94
|
+
lines.splice(range.start + 1, 0, `${KEY} = ${VALUE}`);
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Section absent — append a fresh `[features]` block at EOF. Ensure at least
|
|
99
|
+
// one blank line separates it from prior content for readability.
|
|
100
|
+
const needsLeadingBlank = input.length > 0 && !input.endsWith("\n\n");
|
|
101
|
+
const prefix = input.endsWith("\n") ? "" : "\n";
|
|
102
|
+
const spacer = needsLeadingBlank ? "\n" : "";
|
|
103
|
+
return `${input}${prefix}${spacer}[${SECTION}]\n${KEY} = ${VALUE}\n`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Reverse of mergeCodexHookFeature: strip our `codex_hooks = true` line.
|
|
107
|
+
// - Removes the key line.
|
|
108
|
+
// - Removes an empty `[features]` section if our line was the only content.
|
|
109
|
+
// Returns { text, empty } — empty=true means the TOML is now effectively blank
|
|
110
|
+
// so the caller can delete the file entirely.
|
|
111
|
+
export function unmergeCodexHookFeature(text) {
|
|
112
|
+
const input = text ?? "";
|
|
113
|
+
if (input.trim() === "") return { text: "", empty: true };
|
|
114
|
+
validate(input);
|
|
115
|
+
|
|
116
|
+
const lines = input.split("\n");
|
|
117
|
+
const range = findSectionRange(lines, SECTION);
|
|
118
|
+
if (!range) {
|
|
119
|
+
// No [features] section — nothing to strip.
|
|
120
|
+
return { text: input, empty: input.trim() === "" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const assignRe = keyAssignment(KEY);
|
|
124
|
+
let removedIdx = -1;
|
|
125
|
+
for (let i = range.start + 1; i < range.end; i++) {
|
|
126
|
+
if (assignRe.test(lines[i])) { removedIdx = i; break; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (removedIdx === -1) {
|
|
130
|
+
return { text: input, empty: input.trim() === "" };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.splice(removedIdx, 1);
|
|
134
|
+
|
|
135
|
+
// Re-evaluate the section bounds after removal.
|
|
136
|
+
const newRange = findSectionRange(lines, SECTION);
|
|
137
|
+
if (newRange) {
|
|
138
|
+
let hasContent = false;
|
|
139
|
+
for (let i = newRange.start + 1; i < newRange.end; i++) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
if (line.trim() === "" || /^\s*#/.test(line)) continue;
|
|
142
|
+
hasContent = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (!hasContent) {
|
|
146
|
+
// Remove the header line and any immediately trailing blank lines that
|
|
147
|
+
// were separating this section from the next — otherwise leave them
|
|
148
|
+
// alone.
|
|
149
|
+
let deleteCount = newRange.end - newRange.start;
|
|
150
|
+
while (
|
|
151
|
+
newRange.start + deleteCount < lines.length &&
|
|
152
|
+
lines[newRange.start + deleteCount].trim() === ""
|
|
153
|
+
) {
|
|
154
|
+
deleteCount++;
|
|
155
|
+
// Stop before eating a separator that leads into another section.
|
|
156
|
+
if (
|
|
157
|
+
newRange.start + deleteCount < lines.length &&
|
|
158
|
+
ANY_SECTION_RE.test(lines[newRange.start + deleteCount])
|
|
159
|
+
) break;
|
|
160
|
+
}
|
|
161
|
+
lines.splice(newRange.start, deleteCount);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = lines.join("\n");
|
|
166
|
+
return { text: result, empty: result.trim() === "" };
|
|
167
|
+
}
|
package/src/verify.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// POST /verify — access key server-side validation.
|
|
2
|
+
//
|
|
3
|
+
// `env` (dev|prod) selects the verify endpoint / S3 bucket / download URL.
|
|
4
|
+
// `plan` is the subscription tier (standard | pro | trial) returned by the
|
|
5
|
+
// verify API — do NOT conflate the two.
|
|
6
|
+
|
|
7
|
+
const ENDPOINTS = {
|
|
8
|
+
prod: "https://pytd2jd5p7.execute-api.ap-northeast-1.amazonaws.com",
|
|
9
|
+
dev: "https://zf1kjdpi6a.execute-api.ap-northeast-1.amazonaws.com",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function verifyEndpoint(env = "prod") {
|
|
13
|
+
const base = ENDPOINTS[env] || ENDPOINTS.prod;
|
|
14
|
+
return `${base}/verify`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function downloadBase(env = "prod") {
|
|
18
|
+
const base = ENDPOINTS[env] || ENDPOINTS.prod;
|
|
19
|
+
return `${base}/download`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function verifyKey(accessKey, env = "prod") {
|
|
23
|
+
const url = verifyEndpoint(env);
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify({ access_key: accessKey }),
|
|
30
|
+
});
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error(`verify request failed: ${e.message}`);
|
|
33
|
+
}
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
throw new Error(`verify returned HTTP ${res.status}`);
|
|
36
|
+
}
|
|
37
|
+
const body = await res.json().catch(() => ({}));
|
|
38
|
+
if (body.status !== "active") {
|
|
39
|
+
throw new Error(`access key status is '${body.status || "unknown"}' (expected 'active')`);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
status: body.status,
|
|
43
|
+
plan: body.plan || "standard",
|
|
44
|
+
expires_at: body.expires_at || null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function maskKey(accessKey) {
|
|
49
|
+
if (!accessKey || accessKey.length < 12) return "***";
|
|
50
|
+
return `${accessKey.slice(0, 6)}...${accessKey.slice(-4)}`;
|
|
51
|
+
}
|