@ghl-ai/aw 0.1.37-beta.3 → 0.1.37-beta.31
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/cli.mjs +35 -6
- package/commands/daemon.mjs +32 -23
- package/commands/drop.mjs +46 -48
- package/commands/init.mjs +271 -150
- package/commands/link-project.mjs +52 -10
- package/commands/memory.mjs +237 -0
- package/commands/nuke.mjs +168 -96
- package/commands/pull.mjs +204 -521
- package/commands/push-rules.mjs +6 -5
- package/commands/push.mjs +567 -340
- package/commands/search.mjs +19 -14
- package/commands/status.mjs +101 -83
- package/commands/telemetry.mjs +31 -0
- package/config.mjs +3 -3
- package/constants.mjs +24 -1
- package/ecc.mjs +339 -0
- package/file-tree.mjs +76 -0
- package/fmt.mjs +52 -3
- package/git.mjs +671 -6
- package/hooks.mjs +165 -7
- package/integrate.mjs +29 -15
- package/link.mjs +14 -4
- package/mcp.mjs +170 -36
- package/memory-bridge.mjs +24 -0
- package/package.json +14 -5
- package/render-rules.mjs +273 -67
- package/telemetry.mjs +233 -0
- package/apply.mjs +0 -79
- package/manifest.mjs +0 -64
- package/plan.mjs +0 -147
package/cli.mjs
CHANGED
|
@@ -4,8 +4,9 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join, dirname } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
|
-
import { chalk } from './fmt.mjs';
|
|
7
|
+
import { chalk, CancelError } from './fmt.mjs';
|
|
8
8
|
import { checkForUpdate, notifyUpdate } from './update.mjs';
|
|
9
|
+
import { startSpan } from './telemetry.mjs';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
12
|
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
@@ -21,6 +22,8 @@ const COMMANDS = {
|
|
|
21
22
|
link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
|
|
22
23
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
23
24
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
25
|
+
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
26
|
+
memory: () => import('./commands/memory.mjs').then(m => m.memoryCommand),
|
|
24
27
|
};
|
|
25
28
|
|
|
26
29
|
function parseArgs(argv) {
|
|
@@ -66,14 +69,16 @@ function parseArgs(argv) {
|
|
|
66
69
|
|
|
67
70
|
function printHelp() {
|
|
68
71
|
fmt.banner('aw', {
|
|
69
|
-
|
|
72
|
+
icon: '⟁',
|
|
73
|
+
subtitle: ` ${chalk.hex('#FF6B35')('⟁')} ${chalk.dim('v' + VERSION)} ${chalk.hex('#FF6B35')('Agentic Workspace CLI')} ${chalk.dim('— pull, push & manage agents, skills and more from the registry')}`,
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
const cmd = (c, d) => ` ${chalk.hex('#FF6B35')(c.padEnd(38))} ${chalk.dim(d)}`;
|
|
73
77
|
const sec = (title) => `\n ${chalk.bold.underline(title)}`;
|
|
74
78
|
const help = [
|
|
75
79
|
sec('Setup'),
|
|
76
|
-
cmd('aw init
|
|
80
|
+
cmd('aw init', 'Initialize workspace (platform/ only)'),
|
|
81
|
+
cmd('aw init --namespace <team/sub-team>', 'Add a team namespace (optional)'),
|
|
77
82
|
` ${chalk.dim('Teams: platform, revex, mobile, commerce, leadgen, crm, marketplace, ai')}`,
|
|
78
83
|
` ${chalk.dim('Example: aw init --namespace revex/courses')}`,
|
|
79
84
|
|
|
@@ -93,6 +98,7 @@ function printHelp() {
|
|
|
93
98
|
|
|
94
99
|
sec('Manage'),
|
|
95
100
|
cmd('aw status', 'Show synced paths, modified files & conflicts'),
|
|
101
|
+
cmd('aw link', 'Link current project as a git worktree (wires IDE symlinks)'),
|
|
96
102
|
cmd('aw drop <path>', 'Stop syncing or delete local content'),
|
|
97
103
|
cmd('aw nuke', 'Remove entire .aw_registry/ & start fresh'),
|
|
98
104
|
cmd('aw daemon install', 'Auto-pull on a schedule (macOS launchd / Linux cron)'),
|
|
@@ -100,6 +106,17 @@ function printHelp() {
|
|
|
100
106
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
101
107
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
102
108
|
|
|
109
|
+
sec('Memory'),
|
|
110
|
+
cmd('aw memory store <content>', 'Store a memory (curated)'),
|
|
111
|
+
cmd('aw memory search <query>', 'Search memories'),
|
|
112
|
+
cmd('aw memory pack <query>', 'Get a memory pack (token-budgeted)'),
|
|
113
|
+
cmd('aw memory stats', 'Show memory statistics'),
|
|
114
|
+
|
|
115
|
+
sec('Settings'),
|
|
116
|
+
cmd('aw telemetry status', 'Show telemetry status'),
|
|
117
|
+
cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
|
|
118
|
+
cmd('aw telemetry enable', 'Re-enable analytics'),
|
|
119
|
+
|
|
103
120
|
sec('Examples'),
|
|
104
121
|
'',
|
|
105
122
|
` ${chalk.dim('# Pull content from registry using path')}`,
|
|
@@ -148,9 +165,21 @@ export async function run(argv) {
|
|
|
148
165
|
}
|
|
149
166
|
|
|
150
167
|
if (command && COMMANDS[command]) {
|
|
168
|
+
const span = await startSpan(command, args);
|
|
169
|
+
span.notice();
|
|
151
170
|
args._updateCheck = updateCheck;
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
try {
|
|
172
|
+
const handler = await COMMANDS[command]();
|
|
173
|
+
await handler(args);
|
|
174
|
+
await span.end({ status: 'completed' });
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (err instanceof CancelError) {
|
|
177
|
+
await span.end({ status: 'cancelled', error_type: 'CancelError' });
|
|
178
|
+
process.exit(err.exitCode ?? 1);
|
|
179
|
+
}
|
|
180
|
+
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
181
|
+
throw err;
|
|
182
|
+
}
|
|
154
183
|
notifyUpdate(await updateCheck);
|
|
155
184
|
return;
|
|
156
185
|
}
|
|
@@ -160,5 +189,5 @@ export async function run(argv) {
|
|
|
160
189
|
process.exit(0);
|
|
161
190
|
}
|
|
162
191
|
|
|
163
|
-
fmt.
|
|
192
|
+
fmt.cancelAndExit(`Unknown command: ${command}`);
|
|
164
193
|
}
|
package/commands/daemon.mjs
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
// that silently runs `aw pull` on a schedule without any user interaction.
|
|
3
3
|
|
|
4
4
|
import { existsSync, writeFileSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
5
|
-
import { execSync } from 'node:child_process';
|
|
5
|
+
import { execSync, exec as execCb } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
6
7
|
import { join } from 'node:path';
|
|
7
8
|
import { homedir, platform } from 'node:os';
|
|
8
9
|
import * as fmt from '../fmt.mjs';
|
|
9
10
|
import { chalk } from '../fmt.mjs';
|
|
10
11
|
|
|
12
|
+
const exec = promisify(execCb);
|
|
13
|
+
|
|
11
14
|
const LABEL = 'ai.ghl.aw.pull';
|
|
12
15
|
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${LABEL}.plist`);
|
|
13
16
|
const DEFAULT_INTERVAL = 3600; // 1 hour in seconds
|
|
@@ -22,7 +25,7 @@ function getAwBin() {
|
|
|
22
25
|
|
|
23
26
|
// ── macOS: launchd ──────────────────────────────────────────────────────────
|
|
24
27
|
|
|
25
|
-
function installLaunchd(intervalSeconds) {
|
|
28
|
+
async function installLaunchd(intervalSeconds) {
|
|
26
29
|
const awBin = getAwBin();
|
|
27
30
|
const logDir = join(homedir(), '.aw_registry', 'logs');
|
|
28
31
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
@@ -63,22 +66,24 @@ function installLaunchd(intervalSeconds) {
|
|
|
63
66
|
|
|
64
67
|
writeFileSync(PLIST_PATH, plist);
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
fmt.
|
|
69
|
+
const s = fmt.spinner();
|
|
70
|
+
s.start('Installing daemon...');
|
|
71
|
+
try { await exec(`launchctl unload "${PLIST_PATH}" 2>/dev/null`); } catch { /* not loaded */ }
|
|
72
|
+
await exec(`launchctl load "${PLIST_PATH}"`);
|
|
73
|
+
s.stop(`Daemon installed — runs every ${formatInterval(intervalSeconds)}`);
|
|
74
|
+
fmt.logInfo(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
function uninstallLaunchd() {
|
|
77
|
+
async function uninstallLaunchd() {
|
|
75
78
|
if (!existsSync(PLIST_PATH)) {
|
|
76
79
|
fmt.logStep('No daemon installed.');
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
|
-
|
|
82
|
+
const s = fmt.spinner();
|
|
83
|
+
s.start('Removing daemon...');
|
|
84
|
+
try { await exec(`launchctl unload "${PLIST_PATH}"`); } catch { /* ignore */ }
|
|
80
85
|
unlinkSync(PLIST_PATH);
|
|
81
|
-
|
|
86
|
+
s.stop('Daemon removed.');
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
function statusLaunchd() {
|
|
@@ -102,7 +107,7 @@ function toCronExpression(intervalSeconds) {
|
|
|
102
107
|
return `0 */${hours} * * *`;
|
|
103
108
|
}
|
|
104
109
|
|
|
105
|
-
function installCron(intervalSeconds) {
|
|
110
|
+
async function installCron(intervalSeconds) {
|
|
106
111
|
const awBin = getAwBin();
|
|
107
112
|
const logDir = join(homedir(), '.aw_registry', 'logs');
|
|
108
113
|
if (!existsSync(logDir)) mkdirSync(logDir, { recursive: true });
|
|
@@ -117,17 +122,21 @@ function installCron(intervalSeconds) {
|
|
|
117
122
|
const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
|
|
118
123
|
const updated = cleaned.trimEnd() + '\n' + cronLine + '\n';
|
|
119
124
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
const s = fmt.spinner();
|
|
126
|
+
s.start('Installing cron job...');
|
|
127
|
+
await exec(`echo ${JSON.stringify(updated)} | crontab -`);
|
|
128
|
+
s.stop(`Cron job installed — runs every ${formatInterval(intervalSeconds)}`);
|
|
129
|
+
fmt.logInfo(`Logs: ${chalk.dim(logDir + '/pull.log')}`);
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
function uninstallCron() {
|
|
132
|
+
async function uninstallCron() {
|
|
126
133
|
let current = '';
|
|
127
134
|
try { current = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' }); } catch { return; }
|
|
128
135
|
const cleaned = current.split('\n').filter(l => !l.includes('# aw-daemon')).join('\n');
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
const s = fmt.spinner();
|
|
137
|
+
s.start('Removing cron job...');
|
|
138
|
+
await exec(`echo ${JSON.stringify(cleaned)} | crontab -`);
|
|
139
|
+
s.stop('Cron job removed.');
|
|
131
140
|
}
|
|
132
141
|
|
|
133
142
|
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -148,7 +157,7 @@ function parseInterval(str) {
|
|
|
148
157
|
|
|
149
158
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
150
159
|
|
|
151
|
-
export function daemonCommand(args) {
|
|
160
|
+
export async function daemonCommand(args) {
|
|
152
161
|
const subcommand = args._positional[0] || 'install';
|
|
153
162
|
const interval = parseInterval(args['--interval'] || args._positional[1]);
|
|
154
163
|
const isMac = platform() === 'darwin';
|
|
@@ -158,13 +167,13 @@ export function daemonCommand(args) {
|
|
|
158
167
|
if (subcommand === 'install') {
|
|
159
168
|
fmt.logStep(`Platform: ${isMac ? 'macOS (launchd)' : 'Linux (cron)'}`);
|
|
160
169
|
fmt.logStep(`Interval: every ${formatInterval(interval)}`);
|
|
161
|
-
if (isMac) installLaunchd(interval);
|
|
162
|
-
else installCron(interval);
|
|
170
|
+
if (isMac) await installLaunchd(interval);
|
|
171
|
+
else await installCron(interval);
|
|
163
172
|
fmt.outro(`aw pull will run silently every ${formatInterval(interval)}`);
|
|
164
173
|
|
|
165
174
|
} else if (subcommand === 'uninstall' || subcommand === 'stop') {
|
|
166
|
-
if (isMac) uninstallLaunchd();
|
|
167
|
-
else uninstallCron();
|
|
175
|
+
if (isMac) await uninstallLaunchd();
|
|
176
|
+
else await uninstallCron();
|
|
168
177
|
fmt.outro('Daemon stopped.');
|
|
169
178
|
|
|
170
179
|
} else if (subcommand === 'status') {
|
package/commands/drop.mjs
CHANGED
|
@@ -2,87 +2,85 @@
|
|
|
2
2
|
|
|
3
3
|
import { join, resolve } from 'node:path';
|
|
4
4
|
import { rmSync, existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
5
6
|
import * as config from '../config.mjs';
|
|
6
7
|
import * as fmt from '../fmt.mjs';
|
|
7
8
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { matchesAny } from '../glob.mjs';
|
|
9
9
|
import { resolveInput } from '../paths.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { removeFromSparseCheckout, isValidClone, getLocalRegistryDir } from '../git.mjs';
|
|
11
|
+
import { REGISTRY_DIR, REGISTRY_REPO } from '../constants.mjs';
|
|
12
|
+
import { linkWorkspace } from '../link.mjs';
|
|
11
13
|
|
|
12
14
|
export function dropCommand(args) {
|
|
13
15
|
const input = args._positional?.[0];
|
|
14
16
|
const cwd = process.cwd();
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
const HOME = homedir();
|
|
19
|
+
const AW_HOME = join(HOME, '.aw');
|
|
20
|
+
const GLOBAL_AW_DIR = join(HOME, '.aw_registry');
|
|
21
|
+
const workspaceDir = getLocalRegistryDir(cwd, GLOBAL_AW_DIR);
|
|
16
22
|
|
|
17
23
|
fmt.intro('aw drop');
|
|
18
24
|
|
|
19
25
|
if (!input) {
|
|
20
|
-
fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)
|
|
26
|
+
fmt.cancel('Missing target. Usage:\n aw drop example-team (stop syncing namespace)\n aw drop example-team/skills/example-skill (stop syncing skill)');
|
|
27
|
+
return;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
30
|
+
const repoUrl = `https://github.com/${REGISTRY_REPO}.git`;
|
|
31
|
+
if (!isValidClone(AW_HOME, repoUrl)) {
|
|
32
|
+
fmt.cancel('Registry not initialized. Run: aw init');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
25
35
|
|
|
26
|
-
|
|
36
|
+
const cfg = config.load(GLOBAL_AW_DIR);
|
|
37
|
+
if (!cfg) {
|
|
38
|
+
fmt.cancel('No .sync-config.json found. Run: aw init');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Resolve to registry path
|
|
27
43
|
const resolved = resolveInput(input, workspaceDir);
|
|
28
44
|
const regPath = resolved.registryPath;
|
|
29
45
|
|
|
30
46
|
if (!regPath) {
|
|
31
47
|
fmt.cancel(`Could not resolve "${input}" to a registry path`);
|
|
48
|
+
return;
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
// Check if this path (or a parent) is in config
|
|
51
|
+
// Check if this path (or a parent) is in config
|
|
35
52
|
const isConfigPath = cfg.include.some(p => p === regPath || p.startsWith(regPath + '/'));
|
|
36
53
|
|
|
37
54
|
if (isConfigPath) {
|
|
38
|
-
|
|
55
|
+
// Remove from sparse checkout
|
|
56
|
+
try {
|
|
57
|
+
removeFromSparseCheckout(AW_HOME, [`${REGISTRY_DIR}/${regPath}`]);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
fmt.logWarn(`Could not update sparse checkout: ${e.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
config.removePattern(GLOBAL_AW_DIR, regPath);
|
|
39
63
|
fmt.logSuccess(`Removed ${chalk.cyan(regPath)} from sync config`);
|
|
40
64
|
}
|
|
41
65
|
|
|
42
|
-
//
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
} else if (!isConfigPath) {
|
|
48
|
-
fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
|
|
66
|
+
// Count removed files (they disappear from working tree via sparse checkout)
|
|
67
|
+
const registryAbsPath = join(AW_HOME, REGISTRY_DIR, regPath);
|
|
68
|
+
let removed = 0;
|
|
69
|
+
if (!existsSync(registryAbsPath)) {
|
|
70
|
+
removed = 1; // sparse checkout removed it
|
|
49
71
|
}
|
|
50
72
|
|
|
51
|
-
if (!isConfigPath && removed
|
|
52
|
-
fmt.
|
|
73
|
+
if (!isConfigPath && removed === 0) {
|
|
74
|
+
fmt.cancel(`Nothing found for ${chalk.cyan(regPath)}.\n\n Use ${chalk.dim('aw status')} to see synced paths.`);
|
|
75
|
+
return;
|
|
53
76
|
}
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Find and delete local files whose registry path matches the given path.
|
|
60
|
-
*/
|
|
61
|
-
function deleteMatchingFiles(workspaceDir, path) {
|
|
62
|
-
const manifest = loadManifest(workspaceDir);
|
|
63
|
-
let removed = 0;
|
|
64
|
-
|
|
65
|
-
for (const [manifestKey] of Object.entries(manifest.files)) {
|
|
66
|
-
const registryPath = manifestKeyToRegistryPath(manifestKey);
|
|
67
|
-
|
|
68
|
-
if (matchesAny(registryPath, [path])) {
|
|
69
|
-
const filePath = join(workspaceDir, manifestKey);
|
|
70
|
-
if (existsSync(filePath)) {
|
|
71
|
-
rmSync(filePath, { recursive: true, force: true });
|
|
72
|
-
removed++;
|
|
73
|
-
}
|
|
74
|
-
delete manifest.files[manifestKey];
|
|
75
|
-
}
|
|
78
|
+
if (!isConfigPath) {
|
|
79
|
+
fmt.logWarn(`Path was not in sync config — no sparse checkout change made`);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
82
|
+
// Re-link to remove dead symlinks
|
|
83
|
+
linkWorkspace(HOME, null, { silent: true });
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
* Convert manifest key to registry path.
|
|
84
|
-
* Manifest key now mirrors registry: "platform/agents/architecture-reviewer.md" → "platform/agents/architecture-reviewer"
|
|
85
|
-
*/
|
|
86
|
-
function manifestKeyToRegistryPath(manifestKey) {
|
|
87
|
-
return manifestKey.replace(/\.md$/, '');
|
|
85
|
+
fmt.outro(`⟁ Dropped ${chalk.cyan(regPath)}`);
|
|
88
86
|
}
|