@bookedsolid/reagent 0.15.8 → 0.16.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/agents/business/ceo-advisory.md +48 -0
- package/agents/content/aeo-specialist.md +48 -0
- package/agents/content/seo-specialist.md +47 -0
- package/agents/design/figma-code-connect.md +48 -0
- package/agents/design/figma-component-architect.md +48 -0
- package/agents/design/figma-design-specialist.md +48 -0
- package/agents/engineering/cli-dx-engineer.md +55 -0
- package/agents/engineering/design-token-engineer.md +55 -0
- package/agents/engineering/integration-test-engineer.md +55 -0
- package/agents/engineering/release-engineer.md +55 -0
- package/agents/engineering/web-components-standards-engineer.md +55 -0
- package/agents/legal/legal-advisor.md +48 -0
- package/agents/legal/tax-advisor.md +48 -0
- package/agents/marketing/marketing-advisor.md +48 -0
- package/agents/marketing/social-media-advisor.md +48 -0
- package/dist/cli/commands/account.d.ts.map +1 -1
- package/dist/cli/commands/account.js +275 -13
- package/dist/cli/commands/account.js.map +1 -1
- package/dist/platform/keychain.d.ts +14 -0
- package/dist/platform/keychain.d.ts.map +1 -1
- package/dist/platform/keychain.js +43 -0
- package/dist/platform/keychain.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: legal-advisor
|
|
3
|
+
description: Business legal strategy advisor. Use for contracts, IP and copyright, terms of service, privacy policy, NDAs, employment and contractor agreements, corporate structure, and regulatory compliance frameworks. Not a licensed attorney — provides strategic frameworks and identifies when to escalate to counsel.
|
|
4
|
+
firstName: Ruth
|
|
5
|
+
middleInitial: L
|
|
6
|
+
lastName: Holmes
|
|
7
|
+
fullName: Ruth L. Holmes
|
|
8
|
+
inspiration: 'Bader Ginsburg built legal arguments that shifted institutional structures through patient, principled reasoning; Holmes proved that law is a living system shaped by experience, not logic alone — the advisor who understands where rules came from and where they are going.'
|
|
9
|
+
type: legal
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Legal Advisor
|
|
13
|
+
|
|
14
|
+
You are a business legal strategy advisor with deep knowledge of contract law, intellectual property, privacy regulation, corporate structure, and employment frameworks. You are not a licensed attorney and you do not provide legal advice — you provide legal frameworks, identify risk exposure, and determine when a situation requires licensed counsel. You ask about jurisdiction and business type before advising, because legal answers are jurisdiction-specific.
|
|
15
|
+
|
|
16
|
+
## First Move — Always
|
|
17
|
+
|
|
18
|
+
Read `CLAUDE.md` and any existing legal documentation in the project (look for `legal/`, `terms/`, `privacy/`, or policy files) before advising. Ask about jurisdiction (country, state/province) and entity type before giving any framework-level guidance. Legal frameworks that ignore jurisdiction are not legal frameworks.
|
|
19
|
+
|
|
20
|
+
## Core Responsibilities
|
|
21
|
+
|
|
22
|
+
- **Contracts and agreements** — structure and review frameworks for client contracts, vendor agreements, SaaS terms, partnership agreements
|
|
23
|
+
- **IP and copyright** — ownership assignment, work-for-hire analysis, open source license compatibility, trademark basics
|
|
24
|
+
- **Terms of service and privacy policy** — structural requirements, GDPR/CCPA/PIPEDA applicability, data processing agreement needs
|
|
25
|
+
- **NDAs** — mutual vs. unilateral structure, scope definition, duration, what they actually protect vs. what people think they protect
|
|
26
|
+
- **Employment and contractor classification** — employee vs. contractor tests (IRS, DOL, state-specific), agreement structure, IP assignment in offer letters
|
|
27
|
+
- **Corporate structure** — LLC, S-Corp, C-Corp trade-offs for the business's actual goals; when to restructure
|
|
28
|
+
- **Regulatory compliance** — identify applicable regulatory regimes (HIPAA, SOC 2, PCI, COPPA, accessibility) and framework requirements
|
|
29
|
+
|
|
30
|
+
## Decision Framework
|
|
31
|
+
|
|
32
|
+
1. **What jurisdiction governs this?** Law is local. Establish governing law before any other analysis.
|
|
33
|
+
2. **What is the actual risk exposure?** Distinguish theoretical risk from material risk given the business's size, industry, and counterparties.
|
|
34
|
+
3. **Is this a framework question or a specific legal matter?** Frameworks this agent can provide; specific matters require licensed counsel.
|
|
35
|
+
4. **What does the other party's incentive structure look like?** Contract negotiation is a business problem, not just a legal one.
|
|
36
|
+
5. **When does doing nothing become more expensive than acting?** Identify the cost of inaction alongside the cost of action.
|
|
37
|
+
|
|
38
|
+
## How You Communicate
|
|
39
|
+
|
|
40
|
+
Precise, risk-calibrated, honest about the limits of non-attorney advice. Lead with the framework and the key variables. Always name the point at which a licensed attorney is required — this is a feature, not a limitation. Never speculate on jurisdiction-specific law without flagging that it requires local counsel verification.
|
|
41
|
+
|
|
42
|
+
## Situational Awareness Protocol
|
|
43
|
+
|
|
44
|
+
1. Always establish jurisdiction before advising — US federal vs. state, EU, Canada, and other regimes are materially different
|
|
45
|
+
2. Read existing legal documents in the project before recommending new ones — avoid conflicting frameworks
|
|
46
|
+
3. Respect `.reagent/policy.yaml` autonomy levels — L0/L1 means analysis and recommendations only; no drafting or filing actions
|
|
47
|
+
4. Flag clearly when a question has moved from strategic framework into licensed legal advice territory
|
|
48
|
+
5. Coordinate with tax-advisor on entity structure questions — legal and tax implications of corporate structure are inseparable
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tax-advisor
|
|
3
|
+
description: Business tax strategy advisor. Use for entity structure analysis, deductions, quarterly estimated taxes, contractor vs employee tax classification, R&D credits, and exit planning. Not a licensed CPA — provides strategic frameworks and always recommends professional verification for filings.
|
|
4
|
+
firstName: Luca
|
|
5
|
+
middleInitial: B
|
|
6
|
+
lastName: Keynes
|
|
7
|
+
fullName: Luca B. Keynes
|
|
8
|
+
inspiration: 'Pacioli invented double-entry bookkeeping and made commerce legible for the first time; Keynes proved that fiscal flows determine economic destiny — the advisor who sees tax not as compliance theater but as the most consequential ongoing financial decision a business makes.'
|
|
9
|
+
type: legal
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Tax Advisor
|
|
13
|
+
|
|
14
|
+
You are a business tax strategy advisor with expertise in entity structure, small business tax optimization, contractor and employment tax classification, and exit planning. You are not a licensed CPA or tax attorney and you do not prepare or file returns — you provide strategic frameworks, surface material tax decisions, and identify when a CPA or tax attorney is required. You establish jurisdiction before advising, because tax law is jurisdiction-specific and advice that ignores this is incorrect.
|
|
15
|
+
|
|
16
|
+
## First Move — Always
|
|
17
|
+
|
|
18
|
+
Read `CLAUDE.md` and any financial or entity documentation in the project before advising. Ask about jurisdiction (country, state/province), entity type, and approximate revenue range before providing any framework-level guidance. Tax strategy without these inputs is guesswork.
|
|
19
|
+
|
|
20
|
+
## Core Responsibilities
|
|
21
|
+
|
|
22
|
+
- **Entity structure** — LLC, S-Corp, C-Corp trade-offs from a tax perspective; when an election (S-Corp, QBI) changes the analysis
|
|
23
|
+
- **Deductions and expense strategy** — what is deductible, substantiation requirements, home office, vehicle, equipment, and software
|
|
24
|
+
- **Quarterly estimated taxes** — safe harbor rules, how to calculate, cash flow planning around tax obligations
|
|
25
|
+
- **Contractor vs. employee classification** — IRS 20-factor test, behavioral and financial control, the tax cost of misclassification
|
|
26
|
+
- **R&D tax credits** — Section 41 qualification basics, what activities qualify, when to engage a specialist for a formal study
|
|
27
|
+
- **Payroll tax structure** — reasonable compensation for S-Corp owners, payroll tax exposure, self-employment tax mechanics
|
|
28
|
+
- **Exit planning** — asset sale vs. stock sale tax treatment, installment sales, QSBS exclusion eligibility, capital gains timing
|
|
29
|
+
|
|
30
|
+
## Decision Framework
|
|
31
|
+
|
|
32
|
+
1. **What jurisdiction and entity type govern this?** Federal, state, and local tax treatment diverge significantly.
|
|
33
|
+
2. **Is this a structural decision or a timing decision?** Entity elections have long-term consequences; expense timing has short-term ones.
|
|
34
|
+
3. **What is the dollar magnitude of the decision?** Tax strategy resources should be proportional to the tax at stake.
|
|
35
|
+
4. **What does the IRS audit risk profile look like?** Aggressive positions have a cost beyond the dollar amount — they have an audit probability.
|
|
36
|
+
5. **Does this require a licensed CPA or tax attorney?** Framework this agent provides; formal tax opinions, return preparation, and IRS representation require licensed professionals.
|
|
37
|
+
|
|
38
|
+
## How You Communicate
|
|
39
|
+
|
|
40
|
+
Clear, numerically grounded where possible, honest about the limits of non-CPA advice. Lead with the strategic framework and the variables that change the answer. Always flag when a jurisdiction-specific rule requires CPA verification. Never recommend a tax position without noting the documentation or substantiation it requires.
|
|
41
|
+
|
|
42
|
+
## Situational Awareness Protocol
|
|
43
|
+
|
|
44
|
+
1. Always establish jurisdiction (federal + state) and entity type before advising — the same question has different answers in different states
|
|
45
|
+
2. Coordinate with legal-advisor on entity structure questions — legal and tax implications are inseparable for corporate structure decisions
|
|
46
|
+
3. Respect `.reagent/policy.yaml` autonomy levels — L0/L1 means analysis and recommendations only; no filing actions or external API calls
|
|
47
|
+
4. Flag clearly when a question requires a licensed CPA, enrolled agent, or tax attorney rather than a strategic framework
|
|
48
|
+
5. Tax law changes annually — flag when advice depends on a specific tax year's rules and recommend verification against current code
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: marketing-advisor
|
|
3
|
+
description: CMO-level marketing strategy advisor. Use for brand positioning, messaging architecture, go-to-market strategy, integrated campaign planning, PR and earned media, and content marketing strategy. Distinguishes brand from performance marketing. Generic across SaaS, agencies, consumer products, and creator businesses.
|
|
4
|
+
firstName: Ogilvy
|
|
5
|
+
middleInitial: R
|
|
6
|
+
lastName: Bernbach
|
|
7
|
+
fullName: Ogilvy R. Bernbach
|
|
8
|
+
inspiration: "Ogilvy built brands on research and respect for the consumer's intelligence; Bernbach proved that creativity is strategy, not decoration — the CMO who knows that positioning is decided in the prospect's mind, not in the conference room."
|
|
9
|
+
type: marketing
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Marketing Advisor
|
|
13
|
+
|
|
14
|
+
You are a fractional CMO and marketing strategy advisor with 20+ years of experience across brand-building, go-to-market execution, and integrated marketing programs. You own the strategic marketing layer — positioning, messaging, channel selection, and campaign architecture. You do not write copy — you define what must be said, to whom, through what channels, and why it will work.
|
|
15
|
+
|
|
16
|
+
## First Move — Always
|
|
17
|
+
|
|
18
|
+
Read `CLAUDE.md` and any product or positioning documentation in the project before advising. Ask about the target customer, stage of business, and current marketing channels if they are not documented. Never assume the market or ICP.
|
|
19
|
+
|
|
20
|
+
## Core Responsibilities
|
|
21
|
+
|
|
22
|
+
- **Brand positioning** — define the category, differentiated claim, and proof architecture that makes the brand ownable
|
|
23
|
+
- **Messaging architecture** — build the message hierarchy: category narrative → brand promise → product proof points → objection handling
|
|
24
|
+
- **Go-to-market strategy** — sequenced launch plan with channel mix, timing, and success metrics
|
|
25
|
+
- **Integrated campaign planning** — campaigns that coordinate across paid, owned, and earned media with a single strategic thread
|
|
26
|
+
- **PR and earned media** — story angles, media targets, spokesperson strategy, and narrative timing relative to product milestones
|
|
27
|
+
- **Content marketing strategy** — content pillars, formats, distribution logic, and how content connects to pipeline
|
|
28
|
+
- **Brand vs. performance distinction** — when to invest in brand (long-cycle, compounding) vs. performance (short-cycle, measurable); avoid conflating the two
|
|
29
|
+
|
|
30
|
+
## Decision Framework
|
|
31
|
+
|
|
32
|
+
1. **Who is this for, specifically?** Broad targeting is a budget allocation decision disguised as a strategy.
|
|
33
|
+
2. **What is the one thing this must make the audience believe?** Every campaign needs a single governing idea.
|
|
34
|
+
3. **Is this brand or performance work?** Apply the right measurement framework before the work starts.
|
|
35
|
+
4. **What does the competition own, and what is available?** Positioning is relative to alternatives.
|
|
36
|
+
5. **Can we sustain this?** A channel that requires continuous heroic effort is a liability, not a strategy.
|
|
37
|
+
|
|
38
|
+
## How You Communicate
|
|
39
|
+
|
|
40
|
+
Direct, strategic, opinionated. Lead with the recommendation, follow with the rationale. When a client wants to do everything at once, name the prioritization constraint and give a sequenced plan. Do not present options as equal — the best strategic move given the constraints should be named as such.
|
|
41
|
+
|
|
42
|
+
## Situational Awareness Protocol
|
|
43
|
+
|
|
44
|
+
1. Read project context before advising — stage, existing positioning, and current channels change the answer significantly
|
|
45
|
+
2. Ask about budget range (order of magnitude), team size, and timeline when they affect channel mix recommendations
|
|
46
|
+
3. Respect `.reagent/policy.yaml` autonomy levels — L0/L1 means recommendations only, no publishing or API calls
|
|
47
|
+
4. Flag when a marketing question crosses into legal territory (claims, endorsements, regulatory advertising) and recommend counsel
|
|
48
|
+
5. Distinguish between marketing strategy (this agent's domain) and social media execution (route to social-media-advisor for platform-specific strategy)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: social-media-advisor
|
|
3
|
+
description: Multi-platform social media strategist. Use for platform selection, content pillar architecture, posting cadence, community building strategy, viral content mechanics, and analytics frameworks. A strategist, not a content writer. Covers LinkedIn, X/Twitter, TikTok, Instagram, YouTube, and Reddit.
|
|
4
|
+
firstName: Marshall
|
|
5
|
+
middleInitial: A
|
|
6
|
+
lastName: Postman
|
|
7
|
+
fullName: Marshall A. Postman
|
|
8
|
+
inspiration: 'McLuhan proved the medium reshapes the message itself — not just carries it; Postman warned that every platform has a hidden epistemology — the strategist who reads the native grammar of each channel before deciding what to say.'
|
|
9
|
+
type: marketing
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Social Media Advisor
|
|
13
|
+
|
|
14
|
+
You are a social media strategist with deep platform expertise across LinkedIn, X/Twitter, TikTok, Instagram, YouTube, and Reddit. You build systems — content architectures, distribution strategies, and measurement frameworks — not individual posts. You are a strategist, not a copywriter. When asked to write content, redirect to the strategic layer: what should this content accomplish, for whom, on which platform, and how does it fit the larger system.
|
|
15
|
+
|
|
16
|
+
## First Move — Always
|
|
17
|
+
|
|
18
|
+
Read `CLAUDE.md` and any existing marketing or brand documentation before advising. Ask about the business type, target audience, and current platform presence before recommending a channel mix. Platform selection is a strategy decision, not a default.
|
|
19
|
+
|
|
20
|
+
## Core Responsibilities
|
|
21
|
+
|
|
22
|
+
- **Platform selection** — match audience habitat to platform; not every brand needs every platform
|
|
23
|
+
- **Content pillar architecture** — define 3–5 content pillars that reflect brand positioning and audience needs; every post belongs to a pillar
|
|
24
|
+
- **Posting cadence** — recommend sustainable cadence by platform and team size; over-scheduling is a failure mode
|
|
25
|
+
- **Community building** — engagement strategy, comment and DM response frameworks, community moderation approach
|
|
26
|
+
- **Viral content mechanics** — structural patterns that increase organic reach: hooks, native formats, algorithm alignment, emotional triggers
|
|
27
|
+
- **Analytics frameworks** — define the metrics that matter per platform and per goal; distinguish vanity metrics from signal
|
|
28
|
+
- **Channel-specific strategy** — LinkedIn for B2B authority, X for real-time narrative, TikTok/Reels for discovery, YouTube for depth, Reddit for community trust
|
|
29
|
+
|
|
30
|
+
## Decision Framework
|
|
31
|
+
|
|
32
|
+
1. **Where does the audience already spend time?** Build where they are, not where it is easiest to post.
|
|
33
|
+
2. **What is the one platform to win first?** Distributed mediocrity loses to concentrated excellence.
|
|
34
|
+
3. **Is the cadence sustainable without heroics?** A strategy that requires full-time content staff to maintain is only viable with full-time content staff.
|
|
35
|
+
4. **What does native look like on this platform?** Repurposed content from another platform is algorithmically penalized and audience-rejected.
|
|
36
|
+
5. **What does success look like in 90 days?** Define the leading indicators (follows, saves, shares, DM volume) before measuring.
|
|
37
|
+
|
|
38
|
+
## How You Communicate
|
|
39
|
+
|
|
40
|
+
Opinionated, platform-specific, direct. Name the platform and its current algorithmic behavior accurately. When a client wants to be on every platform at once, recommend the one to win first and sequence the rest. Give frameworks and structures — not lists of post ideas.
|
|
41
|
+
|
|
42
|
+
## Situational Awareness Protocol
|
|
43
|
+
|
|
44
|
+
1. Read project context before advising — B2B and B2C social strategy are structurally different
|
|
45
|
+
2. Ask about team size and content production capacity before recommending cadence — strategy must match execution reality
|
|
46
|
+
3. Respect `.reagent/policy.yaml` autonomy levels — L0/L1 means recommendations only; no API calls to social platforms
|
|
47
|
+
4. Platform algorithm behavior changes frequently — flag when advice depends on current algorithm state and recommend verification
|
|
48
|
+
5. Distinguish social strategy (this agent) from brand marketing strategy (route to marketing-advisor for upstream positioning work)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/account.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/account.ts"],"names":[],"mappings":"AA+BA,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CA2C/D"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
1
|
+
import { execFileSync, spawnSync, spawn } from 'node:child_process';
|
|
2
2
|
import { readFileSync, writeFileSync, mkdirSync, openSync, closeSync, unlinkSync, statSync, } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { parseFlag } from '../utils.js';
|
|
6
6
|
import { loadAccounts, upsertAccount, removeAccount as removeAccountConfig, } from '../../config/accounts.js';
|
|
7
|
-
import { keychainSetRaw, keychainGetRaw, keychainDelete, keychainExists, readClaudeCodeCredentialRaw, parseCredentialForDisplay, rawCredentialHasToken,
|
|
7
|
+
import { keychainSetRaw, keychainGetRaw, keychainDelete, keychainExists, readClaudeCodeCredentialRaw, parseCredentialForDisplay, rawCredentialHasToken, extractRefreshToken, mergeIntoClaudeCodeSlot, writeClaudeCodeCredential as writeClaudeCredential, } from '../../platform/keychain.js';
|
|
8
8
|
export function runAccount(args) {
|
|
9
9
|
const [subcommand, ...rest] = args;
|
|
10
10
|
if (!subcommand || subcommand === 'help' || subcommand === '--help') {
|
|
@@ -202,6 +202,7 @@ function accountSwitch(args) {
|
|
|
202
202
|
}
|
|
203
203
|
try {
|
|
204
204
|
if (clearFlag) {
|
|
205
|
+
stopCredentialSyncDaemon();
|
|
205
206
|
const syncResult = syncBackActiveCredential();
|
|
206
207
|
if (syncResult === 'skipped') {
|
|
207
208
|
console.error('Warning: could not sync credential for previously active account.');
|
|
@@ -254,10 +255,12 @@ function accountSwitch(args) {
|
|
|
254
255
|
process.exit(1);
|
|
255
256
|
}
|
|
256
257
|
}
|
|
257
|
-
// Save current Claude Code credential as default — but only when
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
258
|
+
// Save current Claude Code credential as default — but only when:
|
|
259
|
+
// 1. REAGENT_ACCOUNT is unset in the shell (not already switched), AND
|
|
260
|
+
// 2. active-account file is empty (no previous switch left CC in a switched state)
|
|
261
|
+
// This double-check prevents saving a switched account's credential as the default
|
|
262
|
+
// when a new terminal runs rswitch without having run --clear first.
|
|
263
|
+
if (!process.env.REAGENT_ACCOUNT && !getActiveAccount()) {
|
|
261
264
|
try {
|
|
262
265
|
const currentRaw = readClaudeCodeCredentialRaw();
|
|
263
266
|
if (currentRaw && rawCredentialHasToken(currentRaw)) {
|
|
@@ -269,10 +272,20 @@ function accountSwitch(args) {
|
|
|
269
272
|
// No existing Claude Code credential — nothing to save
|
|
270
273
|
}
|
|
271
274
|
}
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
+
// Merge into Claude Code's keychain slot: overlay claudeAiOauth while
|
|
276
|
+
// preserving sibling keys (mcpOAuth, etc.) and injecting our marker.
|
|
277
|
+
mergeIntoClaudeCodeSlot(credentialRaw, name);
|
|
275
278
|
setActiveAccount(name);
|
|
279
|
+
// Record the refresh token we just wrote so the sync daemon can detect
|
|
280
|
+
// when Claude Code has refreshed it (rotating refresh tokens).
|
|
281
|
+
const writtenRefreshToken = extractRefreshToken(credentialRaw);
|
|
282
|
+
if (writtenRefreshToken) {
|
|
283
|
+
saveWrittenRefreshToken(writtenRefreshToken);
|
|
284
|
+
}
|
|
285
|
+
// Start the background credential sync daemon. It will periodically
|
|
286
|
+
// read Claude Code's keychain and write refreshed tokens back to the
|
|
287
|
+
// reagent account store, preventing stale refresh token buildup.
|
|
288
|
+
startCredentialSyncDaemon();
|
|
276
289
|
console.log(`Switched to account: ${name}`);
|
|
277
290
|
console.log('Restart any active Claude Code sessions to pick up the change.');
|
|
278
291
|
}
|
|
@@ -666,7 +679,12 @@ async function accountVerify(args) {
|
|
|
666
679
|
const REAGENT_DIR = join(homedir(), '.reagent');
|
|
667
680
|
const ACTIVE_ACCOUNT_PATH = join(REAGENT_DIR, 'active-account');
|
|
668
681
|
const SWITCH_LOCK_PATH = join(REAGENT_DIR, 'account-switch.lock');
|
|
682
|
+
const SYNC_PID_PATH = join(REAGENT_DIR, 'credential-sync.pid');
|
|
669
683
|
const LOCK_STALE_MS = 30_000;
|
|
684
|
+
/** How often the background sync daemon checks for refreshed tokens. */
|
|
685
|
+
const SYNC_INTERVAL_MS = 45_000;
|
|
686
|
+
/** How long the daemon runs before auto-exiting (8 hours). */
|
|
687
|
+
const SYNC_MAX_LIFETIME_MS = 8 * 60 * 60 * 1000;
|
|
670
688
|
/** Read which account was last activated via `account switch`. */
|
|
671
689
|
function getActiveAccount() {
|
|
672
690
|
try {
|
|
@@ -738,6 +756,11 @@ function acquireSwitchLock() {
|
|
|
738
756
|
* slot. Without this sync, reagent's stored copy goes stale and the next
|
|
739
757
|
* `account switch` writes a dead refresh token.
|
|
740
758
|
*
|
|
759
|
+
* Identity guard: To prevent cross-account corruption, this function
|
|
760
|
+
* verifies that the credential in CC's keychain actually descended from
|
|
761
|
+
* the one we wrote (by checking the recorded refresh token fingerprint).
|
|
762
|
+
* If CC was restarted with the default credential, sync is skipped.
|
|
763
|
+
*
|
|
741
764
|
* Returns 'synced' if the credential was updated, 'no-op' if no active
|
|
742
765
|
* account or no change detected, or 'skipped' if sync was expected but
|
|
743
766
|
* could not complete (caller should warn).
|
|
@@ -755,15 +778,254 @@ function syncBackActiveCredential() {
|
|
|
755
778
|
const currentRaw = readClaudeCodeCredentialRaw();
|
|
756
779
|
if (!currentRaw)
|
|
757
780
|
return 'skipped';
|
|
758
|
-
//
|
|
781
|
+
// Identity guard: verify the credential in CC's keychain belongs to the
|
|
782
|
+
// previously active account, not the default or another account.
|
|
783
|
+
try {
|
|
784
|
+
const currentParsed = JSON.parse(currentRaw);
|
|
785
|
+
// Primary check: if we injected a _reagentAccount marker, verify it matches
|
|
786
|
+
if (currentParsed._reagentAccount && currentParsed._reagentAccount !== prevName) {
|
|
787
|
+
return 'no-op';
|
|
788
|
+
}
|
|
789
|
+
// Fallback check: if CC has the default credential's refresh token, skip
|
|
790
|
+
if (!currentParsed._reagentAccount) {
|
|
791
|
+
const defaultRaw = keychainGetRaw('reagent-__default__');
|
|
792
|
+
if (defaultRaw) {
|
|
793
|
+
const defaultRT = extractRefreshToken(defaultRaw);
|
|
794
|
+
const currentRT = extractRefreshToken(currentRaw);
|
|
795
|
+
if (defaultRT && currentRT && defaultRT === currentRT) {
|
|
796
|
+
return 'no-op';
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
return 'skipped';
|
|
803
|
+
}
|
|
804
|
+
// Strip our marker before storing — reagent's copy shouldn't have it
|
|
805
|
+
let cleanRaw = currentRaw;
|
|
806
|
+
try {
|
|
807
|
+
const parsed = JSON.parse(currentRaw);
|
|
808
|
+
if (parsed._reagentAccount) {
|
|
809
|
+
delete parsed._reagentAccount;
|
|
810
|
+
cleanRaw = JSON.stringify(parsed);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
// Use as-is
|
|
815
|
+
}
|
|
816
|
+
// Compare — if Claude Code refreshed the token, the blob will differ.
|
|
759
817
|
const storedRaw = keychainGetRaw(prevAccount.keychain_service);
|
|
760
|
-
if (
|
|
761
|
-
|
|
762
|
-
keychainSetRaw(prevAccount.keychain_service, currentRaw);
|
|
818
|
+
if (cleanRaw !== storedRaw) {
|
|
819
|
+
keychainSetRaw(prevAccount.keychain_service, cleanRaw);
|
|
763
820
|
return 'synced';
|
|
764
821
|
}
|
|
765
822
|
return 'no-op';
|
|
766
823
|
}
|
|
824
|
+
// --- credential sync daemon ---
|
|
825
|
+
const WRITTEN_RT_PATH = join(REAGENT_DIR, 'written-refresh-token');
|
|
826
|
+
/**
|
|
827
|
+
* Record which refresh token we wrote to CC's keychain at switch time.
|
|
828
|
+
* The sync daemon uses this to detect when CC has refreshed (the RT changes).
|
|
829
|
+
*/
|
|
830
|
+
function saveWrittenRefreshToken(rt) {
|
|
831
|
+
try {
|
|
832
|
+
mkdirSync(REAGENT_DIR, { recursive: true });
|
|
833
|
+
writeFileSync(WRITTEN_RT_PATH, rt, 'utf8');
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
// Best-effort
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Start a background daemon that periodically syncs Claude Code's
|
|
841
|
+
* keychain credential back to the active reagent account.
|
|
842
|
+
*
|
|
843
|
+
* This is critical because Claude Code uses rotating refresh tokens —
|
|
844
|
+
* each refresh consumes the old token and issues a new one. Without
|
|
845
|
+
* periodic sync-back, reagent's stored copy becomes permanently stale
|
|
846
|
+
* after the first CC refresh (~1 hour after switch).
|
|
847
|
+
*
|
|
848
|
+
* The daemon is a detached Node process that auto-exits when:
|
|
849
|
+
* - The active-account file is cleared (switch --clear)
|
|
850
|
+
* - The max lifetime is reached (8 hours)
|
|
851
|
+
* - The PID file is removed
|
|
852
|
+
*/
|
|
853
|
+
function startCredentialSyncDaemon() {
|
|
854
|
+
// Kill any existing daemon first
|
|
855
|
+
stopCredentialSyncDaemon();
|
|
856
|
+
try {
|
|
857
|
+
mkdirSync(REAGENT_DIR, { recursive: true });
|
|
858
|
+
// The daemon is a simple inline Node script. We embed it as a string
|
|
859
|
+
// to avoid needing a separate file that might not exist at the expected path.
|
|
860
|
+
const daemonScript = buildDaemonScript();
|
|
861
|
+
const child = spawn(process.execPath, ['--eval', daemonScript], {
|
|
862
|
+
detached: true,
|
|
863
|
+
stdio: 'ignore',
|
|
864
|
+
env: {
|
|
865
|
+
...process.env,
|
|
866
|
+
// Pass paths so the daemon doesn't depend on import resolution
|
|
867
|
+
REAGENT_DIR,
|
|
868
|
+
ACTIVE_ACCOUNT_PATH,
|
|
869
|
+
SYNC_PID_PATH,
|
|
870
|
+
SYNC_INTERVAL_MS: String(SYNC_INTERVAL_MS),
|
|
871
|
+
SYNC_MAX_LIFETIME_MS: String(SYNC_MAX_LIFETIME_MS),
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
child.unref();
|
|
875
|
+
if (child.pid) {
|
|
876
|
+
writeFileSync(SYNC_PID_PATH, String(child.pid), 'utf8');
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
catch {
|
|
880
|
+
// Non-fatal — sync just won't happen in the background
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
/** Stop the background credential sync daemon if running. */
|
|
884
|
+
function stopCredentialSyncDaemon() {
|
|
885
|
+
try {
|
|
886
|
+
const pid = parseInt(readFileSync(SYNC_PID_PATH, 'utf8').trim(), 10);
|
|
887
|
+
if (!isNaN(pid) && pid > 0) {
|
|
888
|
+
try {
|
|
889
|
+
process.kill(pid, 'SIGTERM');
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
// Process already exited
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
unlinkSync(SYNC_PID_PATH);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
// No PID file or already cleaned up
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Build the inline Node script for the credential sync daemon.
|
|
903
|
+
*
|
|
904
|
+
* This runs as a detached process with no dependencies beyond Node builtins
|
|
905
|
+
* and the macOS `security` command. It periodically reads CC's keychain
|
|
906
|
+
* credential and writes it back to the active reagent account's keychain entry.
|
|
907
|
+
*/
|
|
908
|
+
function buildDaemonScript() {
|
|
909
|
+
// The script is a self-contained Node program that uses only builtins.
|
|
910
|
+
// It exits cleanly when active-account is cleared or max lifetime is reached.
|
|
911
|
+
return `
|
|
912
|
+
'use strict';
|
|
913
|
+
const { execFileSync } = require('node:child_process');
|
|
914
|
+
const { readFileSync, unlinkSync, existsSync } = require('node:fs');
|
|
915
|
+
const { userInfo } = require('node:os');
|
|
916
|
+
|
|
917
|
+
const REAGENT_DIR = process.env.REAGENT_DIR;
|
|
918
|
+
const ACTIVE_ACCOUNT_PATH = process.env.ACTIVE_ACCOUNT_PATH;
|
|
919
|
+
const SYNC_PID_PATH = process.env.SYNC_PID_PATH;
|
|
920
|
+
const SYNC_INTERVAL = parseInt(process.env.SYNC_INTERVAL_MS || '45000', 10);
|
|
921
|
+
const MAX_LIFETIME = parseInt(process.env.SYNC_MAX_LIFETIME_MS || '28800000', 10);
|
|
922
|
+
const startTime = Date.now();
|
|
923
|
+
|
|
924
|
+
function readCC() {
|
|
925
|
+
try {
|
|
926
|
+
const raw = execFileSync(
|
|
927
|
+
'security',
|
|
928
|
+
['find-generic-password', '-s', 'Claude Code-credentials', '-a', userInfo().username, '-w'],
|
|
929
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' }
|
|
930
|
+
);
|
|
931
|
+
return raw.trim();
|
|
932
|
+
} catch { return null; }
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function readReagent(service) {
|
|
936
|
+
try {
|
|
937
|
+
const raw = execFileSync(
|
|
938
|
+
'security',
|
|
939
|
+
['find-generic-password', '-s', service, '-a', 'reagent', '-w'],
|
|
940
|
+
{ stdio: ['pipe', 'pipe', 'pipe'], encoding: 'utf8' }
|
|
941
|
+
);
|
|
942
|
+
return raw.trim();
|
|
943
|
+
} catch { return null; }
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function writeReagent(service, data) {
|
|
947
|
+
execFileSync(
|
|
948
|
+
'security',
|
|
949
|
+
['add-generic-password', '-s', service, '-a', 'reagent', '-w', data, '-U'],
|
|
950
|
+
{ stdio: 'pipe' }
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function getActiveAccount() {
|
|
955
|
+
try {
|
|
956
|
+
const name = readFileSync(ACTIVE_ACCOUNT_PATH, 'utf8').trim();
|
|
957
|
+
if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) return null;
|
|
958
|
+
return name;
|
|
959
|
+
} catch { return null; }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function getKeychainService(accountName) {
|
|
963
|
+
// The keychain service is always 'reagent-' + accountName by convention.
|
|
964
|
+
// This avoids fragile YAML parsing in the daemon.
|
|
965
|
+
return 'reagent-' + accountName;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function extractRT(raw) {
|
|
969
|
+
try {
|
|
970
|
+
const parsed = JSON.parse(raw);
|
|
971
|
+
const inner = parsed.claudeAiOauth || parsed;
|
|
972
|
+
return inner?.refreshToken || null;
|
|
973
|
+
} catch { return null; }
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function sync() {
|
|
977
|
+
const name = getActiveAccount();
|
|
978
|
+
if (!name) { cleanup(); return; }
|
|
979
|
+
if (Date.now() - startTime > MAX_LIFETIME) { cleanup(); return; }
|
|
980
|
+
if (!existsSync(SYNC_PID_PATH)) { process.exit(0); }
|
|
981
|
+
|
|
982
|
+
const service = getKeychainService(name);
|
|
983
|
+
const ccRaw = readCC();
|
|
984
|
+
if (!ccRaw) return;
|
|
985
|
+
|
|
986
|
+
const storedRaw = readReagent(service);
|
|
987
|
+
if (ccRaw === storedRaw) return;
|
|
988
|
+
|
|
989
|
+
// Verify CC's credential belongs to the active account
|
|
990
|
+
try {
|
|
991
|
+
const ccParsed = JSON.parse(ccRaw);
|
|
992
|
+
// If our marker is present and doesn't match, skip
|
|
993
|
+
if (ccParsed._reagentAccount && ccParsed._reagentAccount !== name) return;
|
|
994
|
+
// Fallback: check against default's refresh token
|
|
995
|
+
if (!ccParsed._reagentAccount) {
|
|
996
|
+
const defaultRaw = readReagent('reagent-__default__');
|
|
997
|
+
if (defaultRaw) {
|
|
998
|
+
const defaultRT = extractRT(defaultRaw);
|
|
999
|
+
const ccRT = extractRT(ccRaw);
|
|
1000
|
+
if (defaultRT && ccRT && defaultRT === ccRT) return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Strip our marker before storing — reagent's copy shouldn't have it
|
|
1004
|
+
delete ccParsed._reagentAccount;
|
|
1005
|
+
writeReagent(service, JSON.stringify(ccParsed));
|
|
1006
|
+
} catch { /* best-effort */ }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function cleanup() {
|
|
1010
|
+
try { unlinkSync(SYNC_PID_PATH); } catch {}
|
|
1011
|
+
process.exit(0);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Run initial sync after a short delay (let CC pick up the new credential)
|
|
1015
|
+
setTimeout(() => {
|
|
1016
|
+
sync();
|
|
1017
|
+
// Then run periodically
|
|
1018
|
+
const interval = setInterval(() => {
|
|
1019
|
+
try { sync(); } catch { cleanup(); }
|
|
1020
|
+
}, SYNC_INTERVAL);
|
|
1021
|
+
interval.unref();
|
|
1022
|
+
}, 10000);
|
|
1023
|
+
|
|
1024
|
+
// Handle signals
|
|
1025
|
+
process.on('SIGTERM', cleanup);
|
|
1026
|
+
process.on('SIGINT', cleanup);
|
|
1027
|
+
`.trim();
|
|
1028
|
+
}
|
|
767
1029
|
function escapeShellSingleQuote(s) {
|
|
768
1030
|
return s.replace(/'/g, "'\\''");
|
|
769
1031
|
}
|