Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/builtin-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function getBuiltinAgentsDir(): string {
export async function deployBuiltinAgents(
teamConfig: TeamaiConfig,
localConfig?: LocalConfig,
options?: { skipRecall?: boolean },
): Promise<number> {
const builtinDir = getBuiltinAgentsDir();
if (!await pathExists(builtinDir)) {
Expand All @@ -69,7 +70,9 @@ export async function deployBuiltinAgents(
return 0;
}

const agentFiles = entries.filter((f) => f.endsWith('.md') && !f.startsWith('.'));
const agentFiles = entries
.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
.filter((f) => !(options?.skipRecall && f === 'teamai-recall.md'));
if (agentFiles.length === 0) return 0;

const baseDir = localConfig ? resolveBaseDir(localConfig) : (process.env.HOME ?? '');
Expand Down
8 changes: 6 additions & 2 deletions src/builtin-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ export const EXCLUDED_RULE_NAMES = new Set<string>([
*
* @returns Number of tool directories that received built-in rules.
*/
export async function deployBuiltinRules(teamConfig: TeamaiConfig, localConfig?: LocalConfig): Promise<number> {
export async function deployBuiltinRules(
teamConfig: TeamaiConfig,
localConfig?: LocalConfig,
options?: { skipRecall?: boolean },
): Promise<number> {
const baseDir = localConfig ? resolveBaseDir(localConfig) : (process.env.HOME ?? '');
let deployed = 0;

const builtinRules: Array<{ name: string; content: string }> = [
{ name: 'teamai-recall', content: TEAMAI_RECALL_RULE_CONTENT },
];
].filter(r => !(options?.skipRecall && r.name === 'teamai-recall'));

for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
if (!toolPath.rules) continue;
Expand Down
29 changes: 28 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ program

// ─── Recall commands ─────────────────────────────────────

program
const recallCmd = program
.command('recall [query...]')
.description('Search team learnings knowledge base')
.option('--depth <level>', 'Recall depth: route (entry-points only) | context (module-level, default) | lookup (full graph traversal)', 'context')
Expand All @@ -552,6 +552,33 @@ program
await recall(query, { ...globalOpts, depth: cmdOpts.depth });
});

recallCmd
.command('disable')
.description('Disable automatic knowledge-base recall')
.action(async () => {
const globalOpts = program.opts() as GlobalOptions;
const { recallDisable } = await import('./recall-toggle.js');
await recallDisable(globalOpts);
});

recallCmd
.command('enable')
.description('Enable automatic knowledge-base recall')
.action(async () => {
const globalOpts = program.opts() as GlobalOptions;
const { recallEnable } = await import('./recall-toggle.js');
await recallEnable(globalOpts);
});

recallCmd
.command('status')
.description('Show recall feature status')
.action(async () => {
const globalOpts = program.opts() as GlobalOptions;
const { recallStatus } = await import('./recall-toggle.js');
await recallStatus(globalOpts);
});

program
.command('todowrite-hint', { hidden: true })

Expand Down
14 changes: 9 additions & 5 deletions src/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
CultureFrontmatterSchema,
resolveBaseDir,
getTeamaiHome,
isRecallEnabled,
} from './types.js';
import type { CultureFrontmatter } from './types.js';
import { loadRolesManifest, resolveRoleResourceNamespaces, type ResourceNamespaces } from './roles.js';
Expand Down Expand Up @@ -316,8 +317,9 @@ async function pullForScope(
if (!options.dryRun) {
const cfg = await loadTeamConfig(localConfig.repo.localPath);
if (cfg) {
try { const { deployBuiltinAgents } = await import('./builtin-agents.js'); await deployBuiltinAgents(cfg, localConfig); } catch {}
try { const { deployBuiltinRules } = await import('./builtin-rules.js'); await deployBuiltinRules(cfg, localConfig); } catch {}
const skipRecall = !isRecallEnabled(localConfig, cfg);
try { const { deployBuiltinAgents } = await import('./builtin-agents.js'); await deployBuiltinAgents(cfg, localConfig, { skipRecall }); } catch {}
try { const { deployBuiltinRules } = await import('./builtin-rules.js'); await deployBuiltinRules(cfg, localConfig, { skipRecall }); } catch {}
try { const { deployBuiltinSkills } = await import('./builtin-skills.js'); await deployBuiltinSkills(cfg, localConfig, { reportingOnly }); } catch {}
}
}
Expand Down Expand Up @@ -710,7 +712,7 @@ async function pullForScope(
// configured. Tools without subagent support (cursor / codex / openclaw /
// workbuddy) are skipped — for them the recall flow runs purely via the
// TodoWrite hint hook and the manual `teamai recall` command.
if (!options.dryRun) {
if (!options.dryRun && isRecallEnabled(localConfig, freshConfig)) {
try {
const baseDir = resolveBaseDir(localConfig);
const recallBlock = compileRecallRulesBlock();
Expand Down Expand Up @@ -758,7 +760,8 @@ async function pullForScope(
if (!options.dryRun) {
try {
const { deployBuiltinRules } = await import('./builtin-rules.js');
const deployed = await deployBuiltinRules(freshConfig, localConfig);
const skipRecall = !isRecallEnabled(localConfig, freshConfig);
const deployed = await deployBuiltinRules(freshConfig, localConfig, { skipRecall });
if (deployed > 0) {
log.debug(`[${scopeLabel}] Deployed built-in rules to ${deployed} tool(s)`);
}
Expand All @@ -771,7 +774,8 @@ async function pullForScope(
if (!options.dryRun) {
try {
const { deployBuiltinAgents } = await import('./builtin-agents.js');
const deployed = await deployBuiltinAgents(freshConfig, localConfig);
const skipRecall = !isRecallEnabled(localConfig, freshConfig);
const deployed = await deployBuiltinAgents(freshConfig, localConfig, { skipRecall });
if (deployed > 0) {
log.debug(`[${scopeLabel}] Deployed built-in agents to ${deployed} location(s)`);
}
Expand Down
126 changes: 126 additions & 0 deletions src/recall-toggle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import path from 'node:path';
import { autoDetectInit, saveLocalConfigForScope } from './config.js';
import { log } from './utils/logger.js';
import { readFileSafe, writeFile, remove, pathExists } from './utils/fs.js';
import { ResourceHandler } from './resources/base.js';
import {
resolveBaseDir,
isRecallEnabled,
TEAMAI_RECALL_RULES_START,
TEAMAI_RECALL_RULES_END,
type GlobalOptions,
type TeamaiConfig,
type LocalConfig,
} from './types.js';

async function removeRecallArtifacts(teamConfig: TeamaiConfig, localConfig: LocalConfig): Promise<void> {
const baseDir = resolveBaseDir(localConfig);

for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
// Remove recall rule file
if (toolPath.rules) {
const ruleFile = path.join(baseDir, toolPath.rules, 'teamai-recall.md');
if (await pathExists(ruleFile)) {
await remove(ruleFile);
log.debug(`Removed recall rule from ${tool}`);
}
}

// Remove recall agent file
if (toolPath.agents) {
const agentFile = path.join(baseDir, toolPath.agents, 'teamai-recall.md');
if (await pathExists(agentFile)) {
await remove(agentFile);
log.debug(`Removed recall agent from ${tool}`);
}
}

// Remove recall block from CLAUDE.md
if (toolPath.claudemd) {
const claudeMdPath = path.join(baseDir, toolPath.claudemd);
const content = await readFileSafe(claudeMdPath);
if (content && content.includes(TEAMAI_RECALL_RULES_START)) {
const startIdx = content.indexOf(TEAMAI_RECALL_RULES_START);
const endIdx = content.indexOf(TEAMAI_RECALL_RULES_END);
if (startIdx !== -1 && endIdx !== -1) {
const before = content.substring(0, startIdx).replace(/\n+$/, '\n');
const after = content.substring(endIdx + TEAMAI_RECALL_RULES_END.length).replace(/^\n+/, '\n');
const cleaned = (before + after).trim();
if (cleaned.length === 0) {
await remove(claudeMdPath);
} else {
await writeFile(claudeMdPath, cleaned + '\n');
}
log.debug(`Removed recall rules block from ${tool} CLAUDE.md`);
}
}
}
}
}

async function deployRecallArtifacts(teamConfig: TeamaiConfig, localConfig: LocalConfig): Promise<void> {
const { deployBuiltinRules } = await import('./builtin-rules.js');
const { deployBuiltinAgents } = await import('./builtin-agents.js');

await deployBuiltinRules(teamConfig, localConfig, { skipRecall: false });
await deployBuiltinAgents(teamConfig, localConfig, { skipRecall: false });

// Inject recall rules block into CLAUDE.md for Tier-1 tools
const { injectClaudeMdSection } = await import('./utils/claudemd.js');
const { compileRecallRulesBlock } = await import('./pull.js');
const baseDir = resolveBaseDir(localConfig);
const recallBlock = compileRecallRulesBlock();

for (const [tool, toolPath] of Object.entries(teamConfig.toolPaths)) {
if (!toolPath.claudemd || !toolPath.agents) continue;
if (!await ResourceHandler.isToolInstalled(toolPath.agents, baseDir)) continue;

const claudeMdPath = path.join(baseDir, toolPath.claudemd);
try {
await injectClaudeMdSection(
claudeMdPath,
TEAMAI_RECALL_RULES_START,
TEAMAI_RECALL_RULES_END,
recallBlock,
);
} catch {
// best-effort
}
}
}

export async function recallDisable(_opts: GlobalOptions): Promise<void> {
const { localConfig, teamConfig } = await autoDetectInit();

const updated = { ...localConfig, recallEnabled: false };
await saveLocalConfigForScope(updated, localConfig.scope, localConfig.projectRoot);

await removeRecallArtifacts(teamConfig, localConfig);
log.success('Recall disabled. AI tools will no longer auto-search the knowledge base.');
}

export async function recallEnable(_opts: GlobalOptions): Promise<void> {
const { localConfig, teamConfig } = await autoDetectInit();

const updated = { ...localConfig, recallEnabled: true };
await saveLocalConfigForScope(updated, localConfig.scope, localConfig.projectRoot);

await deployRecallArtifacts(teamConfig, localConfig);
log.success('Recall enabled. AI tools will auto-search the knowledge base before tasks.');
}

export async function recallStatus(_opts: GlobalOptions): Promise<void> {
const { localConfig, teamConfig } = await autoDetectInit();

const effective = isRecallEnabled(localConfig, teamConfig);
const teamSetting = teamConfig.sharing?.recall?.enabled ?? false;
const userOverride = localConfig.recallEnabled;

console.log(`Recall: ${effective ? 'enabled' : 'disabled'}`);
console.log(` Team config (sharing.recall.enabled): ${teamSetting}`);
if (userOverride !== undefined) {
console.log(` User override (recallEnabled): ${userOverride}`);
} else {
console.log(` User override: not set (using team default)`);
}
}
21 changes: 21 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export const SharingConfigSchema = z.object({
/** Restrict team hook commands to scripts under ~/.teamai/team-scripts/. */
requireTeamScripts: z.boolean().default(false),
}).optional(),
recall: z.object({
enabled: z.boolean().default(false),
}).optional(),
});

/** Defaulted view of the optional `sharing.hooks` config. */
Expand All @@ -55,6 +58,22 @@ export function getHooksSharing(config: { sharing?: { hooks?: { autoApply?: bool
};
}

/** Defaulted view of the optional `sharing.recall` config. */
export function getRecallSharing(config: { sharing?: { recall?: { enabled?: boolean } } }): {
enabled: boolean;
} {
return { enabled: config.sharing?.recall?.enabled ?? false };
}

/** Resolve whether recall is enabled: user override > team config > default (false). */
export function isRecallEnabled(
localConfig: { recallEnabled?: boolean },
teamConfig: { sharing?: { recall?: { enabled?: boolean } } },
): boolean {
if (localConfig.recallEnabled !== undefined) return localConfig.recallEnabled;
return getRecallSharing(teamConfig).enabled;
}

// ─── Source config (cross-team subscription) ─────────
//
// Data flow:
Expand Down Expand Up @@ -166,6 +185,8 @@ export const LocalConfigSchema = z.object({
projectRoot: z.string().optional(),
/** Tags the user has subscribed to. If empty/undefined, pull all resources. */
subscribedTags: z.array(z.string()).optional(),
/** User-level override for recall feature. When set, takes precedence over team config. */
recallEnabled: z.boolean().optional(),
});

export type LocalConfig = z.infer<typeof LocalConfigSchema>;
Expand Down
Loading