@ghl-ai/aw 0.1.36 → 0.1.37-beta.4
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 +17 -2
- package/commands/init.mjs +1 -1
- package/commands/push.mjs +10 -3
- package/commands/telemetry.mjs +31 -0
- package/constants.mjs +3 -0
- package/hooks.mjs +5 -5
- package/package.json +3 -2
- package/telemetry.mjs +219 -0
package/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
6
|
import * as fmt from './fmt.mjs';
|
|
7
7
|
import { chalk } 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;
|
|
@@ -20,6 +21,7 @@ const COMMANDS = {
|
|
|
20
21
|
link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
|
|
21
22
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
22
23
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
24
|
+
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
23
25
|
};
|
|
24
26
|
|
|
25
27
|
function parseArgs(argv) {
|
|
@@ -101,6 +103,11 @@ function printHelp() {
|
|
|
101
103
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
102
104
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
103
105
|
|
|
106
|
+
sec('Settings'),
|
|
107
|
+
cmd('aw telemetry status', 'Show telemetry status'),
|
|
108
|
+
cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
|
|
109
|
+
cmd('aw telemetry enable', 'Re-enable analytics'),
|
|
110
|
+
|
|
104
111
|
sec('Examples'),
|
|
105
112
|
'',
|
|
106
113
|
` ${chalk.dim('# Pull content from registry using path')}`,
|
|
@@ -147,9 +154,17 @@ export async function run(argv) {
|
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
if (command && COMMANDS[command]) {
|
|
157
|
+
const span = startSpan(command, args);
|
|
158
|
+
span.notice();
|
|
150
159
|
args._updateCheck = updateCheck;
|
|
151
|
-
|
|
152
|
-
|
|
160
|
+
try {
|
|
161
|
+
const handler = await COMMANDS[command]();
|
|
162
|
+
await handler(args);
|
|
163
|
+
await span.end({ status: 'completed' });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
153
168
|
notifyUpdate(await updateCheck);
|
|
154
169
|
return;
|
|
155
170
|
}
|
package/commands/init.mjs
CHANGED
|
@@ -74,7 +74,7 @@ function installIdeTasks() {
|
|
|
74
74
|
{
|
|
75
75
|
label: 'aw: sync registry',
|
|
76
76
|
type: 'shell',
|
|
77
|
-
command: 'aw init --silent',
|
|
77
|
+
command: 'AW_TRIGGER=ide:task aw init --silent',
|
|
78
78
|
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
79
79
|
runOptions: { runOn: 'folderOpen' },
|
|
80
80
|
problemMatcher: [],
|
package/commands/push.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// commands/push.mjs — Push local agents/skills to registry via PR using persistent git clone
|
|
2
2
|
|
|
3
3
|
import { existsSync, statSync, readFileSync, appendFileSync } from 'node:fs';
|
|
4
|
-
import { join } from 'node:path';
|
|
4
|
+
import { join, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { exec as execCb, execFile as execFileCb } from 'node:child_process';
|
|
6
7
|
import { promisify } from 'node:util';
|
|
7
8
|
import { homedir } from 'node:os';
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
logAheadOfMain,
|
|
26
27
|
} from '../git.mjs';
|
|
27
28
|
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
31
|
+
|
|
28
32
|
const PUSHABLE_TYPES = ['agents', 'skills', 'commands', 'evals'];
|
|
29
33
|
|
|
30
34
|
// ── PR content generation ────────────────────────────────────────────
|
|
@@ -187,11 +191,14 @@ function generateCommitMsg(files) {
|
|
|
187
191
|
const deletedParts = Object.entries(groupBy(deleted, 'type')).map(([t, items]) => `${items.length} ${singular(t, items.length)} removed`);
|
|
188
192
|
const countParts = [...addedParts, ...deletedParts];
|
|
189
193
|
|
|
194
|
+
const version = VERSION;
|
|
195
|
+
const trailer = `\n\nGenerated-By: aw/${version}`;
|
|
196
|
+
|
|
190
197
|
if (files.length === 1) {
|
|
191
198
|
const f = files[0];
|
|
192
|
-
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}`;
|
|
199
|
+
return `registry: ${f.deleted ? 'remove' : 'add'} ${f.type}/${f.slug} ${f.deleted ? 'from' : 'to'} ${f.namespace}${trailer}`;
|
|
193
200
|
}
|
|
194
|
-
return `registry: sync ${files.length} files (${countParts.join(', ')})`;
|
|
201
|
+
return `registry: sync ${files.length} files (${countParts.join(', ')})${trailer}`;
|
|
195
202
|
}
|
|
196
203
|
|
|
197
204
|
// ── Batch file collection from folder ────────────────────────────────
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// commands/telemetry.mjs — `aw telemetry [enable|disable|status]`
|
|
2
|
+
|
|
3
|
+
import { enableTelemetry, disableTelemetry, getStatus } from '../telemetry.mjs';
|
|
4
|
+
import * as fmt from '../fmt.mjs';
|
|
5
|
+
import { chalk } from '../fmt.mjs';
|
|
6
|
+
|
|
7
|
+
export async function telemetryCommand(args) {
|
|
8
|
+
const sub = args._positional?.[0];
|
|
9
|
+
|
|
10
|
+
if (sub === 'disable') {
|
|
11
|
+
disableTelemetry();
|
|
12
|
+
fmt.logSuccess('Telemetry disabled. No anonymous usage data will be sent.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (sub === 'enable') {
|
|
17
|
+
enableTelemetry();
|
|
18
|
+
fmt.logSuccess('Telemetry enabled. Anonymous usage stats help improve aw.');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// status (default)
|
|
23
|
+
const status = getStatus();
|
|
24
|
+
fmt.intro('aw telemetry');
|
|
25
|
+
fmt.logStep(`Status: ${status.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
26
|
+
fmt.logStep(`Machine ID: ${chalk.dim(status.machine_id)}`);
|
|
27
|
+
fmt.logStep(`Config: ${chalk.dim(status.config_path)}`);
|
|
28
|
+
fmt.logMessage('');
|
|
29
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
|
|
30
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
|
|
31
|
+
}
|
package/constants.mjs
CHANGED
|
@@ -24,3 +24,6 @@ export const DOCS_SOURCE_DIR = 'content';
|
|
|
24
24
|
|
|
25
25
|
/** Persistent git clone root — ~/.aw/ */
|
|
26
26
|
export const AW_HOME = join(homedir(), '.aw');
|
|
27
|
+
|
|
28
|
+
/** Telemetry endpoint — override with AW_TELEMETRY_URL env var */
|
|
29
|
+
export const TELEMETRY_URL = process.env.AW_TELEMETRY_URL || 'https://aw-telemetry.ghl.ai/v1/events';
|
package/hooks.mjs
CHANGED
|
@@ -55,17 +55,17 @@ if command -v aw >/dev/null 2>&1; then
|
|
|
55
55
|
# not the project repo that triggered this hook. Using git's own list of
|
|
56
56
|
# local env vars is more robust than hardcoding specific names.
|
|
57
57
|
unset $(git rev-parse --local-env-vars 2>/dev/null)
|
|
58
|
-
aw pull --silent >/dev/null 2>&1 &
|
|
58
|
+
AW_TRIGGER=hook:post-merge aw pull --silent >/dev/null 2>&1 &
|
|
59
59
|
fi`);
|
|
60
60
|
|
|
61
61
|
const POST_CHECKOUT = makeDispatcher('post-checkout', `\
|
|
62
62
|
# Unset ALL git env vars so aw's "git -C ~/.aw" runs against the correct repo.
|
|
63
63
|
unset $(git rev-parse --local-env-vars 2>/dev/null)
|
|
64
64
|
if [ -d "$HOME/.aw" ] && [ ! -d ".aw" ] && [ -d ".git" ] && command -v aw >/dev/null 2>&1; then
|
|
65
|
-
aw link >/dev/null 2>&1 &
|
|
65
|
+
AW_TRIGGER=hook:post-checkout aw link >/dev/null 2>&1 &
|
|
66
66
|
fi
|
|
67
67
|
if command -v aw >/dev/null 2>&1; then
|
|
68
|
-
aw pull --silent >/dev/null 2>&1 &
|
|
68
|
+
AW_TRIGGER=hook:post-checkout aw pull --silent >/dev/null 2>&1 &
|
|
69
69
|
fi`);
|
|
70
70
|
|
|
71
71
|
// post-commit: written separately — needs different guard logic than other hooks.
|
|
@@ -83,7 +83,7 @@ case "$(pwd)" in /tmp/aw-*|/var/folders/*/aw-*) exit 0 ;; esac
|
|
|
83
83
|
# Committing inside .aw/ worktree itself → refresh from parent project dir
|
|
84
84
|
case "$(pwd)" in */.aw)
|
|
85
85
|
if command -v aw >/dev/null 2>&1; then
|
|
86
|
-
(cd "$(dirname "$(pwd)")" && aw link >/dev/null 2>&1) &
|
|
86
|
+
(cd "$(dirname "$(pwd)")" && AW_TRIGGER=hook:post-commit aw link >/dev/null 2>&1) &
|
|
87
87
|
fi
|
|
88
88
|
exit 0
|
|
89
89
|
;;
|
|
@@ -91,7 +91,7 @@ esac
|
|
|
91
91
|
|
|
92
92
|
# Committing in a project that has a .aw/ worktree → refresh symlinks
|
|
93
93
|
if [ -f ".aw/.git" ] && command -v aw >/dev/null 2>&1; then
|
|
94
|
-
aw link >/dev/null 2>&1 &
|
|
94
|
+
AW_TRIGGER=hook:post-commit aw link >/dev/null 2>&1 &
|
|
95
95
|
fi
|
|
96
96
|
|
|
97
97
|
# Chain to previous hooksPath
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ghl-ai/aw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37-beta.4",
|
|
4
4
|
"description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": "bin.js",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"apply.mjs",
|
|
24
24
|
"update.mjs",
|
|
25
25
|
"hooks.mjs",
|
|
26
|
-
"ecc.mjs"
|
|
26
|
+
"ecc.mjs",
|
|
27
|
+
"telemetry.mjs"
|
|
27
28
|
],
|
|
28
29
|
"engines": {
|
|
29
30
|
"node": ">=18.0.0"
|
package/telemetry.mjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// telemetry.mjs — Anonymous usage analytics. Zero new dependencies.
|
|
2
|
+
//
|
|
3
|
+
// Span-based: each command sends command_started + command_completed/command_failed,
|
|
4
|
+
// linked by a run_id. Orphaned started events = stuck/killed commands.
|
|
5
|
+
//
|
|
6
|
+
// Opt out: AW_TELEMETRY_DISABLED=1, DO_NOT_TRACK=1, or `aw telemetry disable`.
|
|
7
|
+
|
|
8
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
+
import { hostname, userInfo, platform, arch, release } from 'node:os';
|
|
10
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'node:fs';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { TELEMETRY_URL, AW_HOME } from './constants.mjs';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const VERSION = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8')).version;
|
|
18
|
+
|
|
19
|
+
const CONFIG_PATH = join(AW_HOME, '.telemetry');
|
|
20
|
+
|
|
21
|
+
// ── Config ──────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export function generateMachineId() {
|
|
24
|
+
const raw = `${hostname()}:${userInfo().username}`;
|
|
25
|
+
return createHash('sha256').update(raw).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadConfig() {
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(CONFIG_PATH)) {
|
|
31
|
+
return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
|
|
32
|
+
}
|
|
33
|
+
} catch { /* corrupt file — recreate */ }
|
|
34
|
+
|
|
35
|
+
const config = {
|
|
36
|
+
machine_id: generateMachineId(),
|
|
37
|
+
enabled: true,
|
|
38
|
+
noticed: false,
|
|
39
|
+
};
|
|
40
|
+
saveConfig(config);
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function saveConfig(config) {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(dirname(CONFIG_PATH), { recursive: true });
|
|
47
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
|
|
48
|
+
} catch { /* best effort — don't break CLI */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Opt-out detection ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export function isDisabled(config) {
|
|
54
|
+
if (process.env.DO_NOT_TRACK === '1') return true;
|
|
55
|
+
if (process.env.AW_TELEMETRY_DISABLED === '1') return true;
|
|
56
|
+
if (config && config.enabled === false) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Environment collection ──────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
function getRegistryHead() {
|
|
63
|
+
try {
|
|
64
|
+
return execSync('git -C "' + AW_HOME + '" rev-parse --short HEAD', {
|
|
65
|
+
encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 2000,
|
|
66
|
+
}).trim();
|
|
67
|
+
} catch { return null; }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function detectIDEs() {
|
|
71
|
+
const ides = [];
|
|
72
|
+
const cwd = process.cwd();
|
|
73
|
+
const checks = [
|
|
74
|
+
['.cursor', 'cursor'],
|
|
75
|
+
['.vscode', 'vscode'],
|
|
76
|
+
['.claude', 'claude'],
|
|
77
|
+
['.codex', 'codex'],
|
|
78
|
+
];
|
|
79
|
+
for (const [dir, name] of checks) {
|
|
80
|
+
try {
|
|
81
|
+
if (existsSync(join(cwd, dir))) ides.push(name);
|
|
82
|
+
} catch { /* skip */ }
|
|
83
|
+
}
|
|
84
|
+
return ides;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getNamespace() {
|
|
88
|
+
try {
|
|
89
|
+
const cfgPath = join(AW_HOME, '.aw_registry', '.sync-config.json');
|
|
90
|
+
if (existsSync(cfgPath)) {
|
|
91
|
+
const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
|
|
92
|
+
return cfg.namespace || null;
|
|
93
|
+
}
|
|
94
|
+
} catch { /* skip */ }
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function projectHash() {
|
|
99
|
+
return createHash('sha256').update(process.cwd()).digest('hex').slice(0, 8);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function collectEnv(config) {
|
|
103
|
+
return {
|
|
104
|
+
machine_id: config.machine_id,
|
|
105
|
+
aw_version: VERSION,
|
|
106
|
+
node_version: process.version,
|
|
107
|
+
os: platform(),
|
|
108
|
+
arch: arch(),
|
|
109
|
+
is_ci: !!(process.env.CI || process.env.CONTINUOUS_INTEGRATION || process.env.BUILD_NUMBER),
|
|
110
|
+
namespace: getNamespace(),
|
|
111
|
+
ides_detected: detectIDEs(),
|
|
112
|
+
project_hash: projectHash(),
|
|
113
|
+
trigger: process.env.AW_TRIGGER || 'interactive',
|
|
114
|
+
registry_head: getRegistryHead(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Network ─────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export async function send(payload) {
|
|
121
|
+
try {
|
|
122
|
+
await fetch(TELEMETRY_URL, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: { 'Content-Type': 'application/json' },
|
|
125
|
+
body: JSON.stringify(payload),
|
|
126
|
+
signal: AbortSignal.timeout(3000),
|
|
127
|
+
});
|
|
128
|
+
} catch {
|
|
129
|
+
// Swallow ALL errors — telemetry must never break the CLI
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Span API ────────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export function startSpan(command, args) {
|
|
136
|
+
const config = loadConfig();
|
|
137
|
+
const disabled = isDisabled(config);
|
|
138
|
+
const runId = randomUUID();
|
|
139
|
+
const startTime = Date.now();
|
|
140
|
+
|
|
141
|
+
// Extract flags and positional args for the payload
|
|
142
|
+
const flags = [];
|
|
143
|
+
const positional = [];
|
|
144
|
+
if (args) {
|
|
145
|
+
for (const [key, val] of Object.entries(args)) {
|
|
146
|
+
if (key === '_positional') {
|
|
147
|
+
positional.push(...(val || []));
|
|
148
|
+
} else if (key.startsWith('-') || key.startsWith('--')) {
|
|
149
|
+
if (val === true) flags.push(key);
|
|
150
|
+
else flags.push(`${key}=${val}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const env = disabled ? null : collectEnv(config);
|
|
156
|
+
|
|
157
|
+
// Fire command_started (non-blocking)
|
|
158
|
+
if (!disabled) {
|
|
159
|
+
send({
|
|
160
|
+
event: 'command_started',
|
|
161
|
+
run_id: runId,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
env,
|
|
164
|
+
command: { name: command, args: positional, flags },
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
runId,
|
|
170
|
+
|
|
171
|
+
/** Show one-time first-run notice (only in interactive/TTY mode) */
|
|
172
|
+
notice() {
|
|
173
|
+
if (disabled || config.noticed) return;
|
|
174
|
+
if (args?.['--silent'] || !process.stderr.isTTY) return;
|
|
175
|
+
console.error('\u2139 Telemetry is on \u2014 anonymous usage stats help improve aw. Opt out: aw telemetry disable');
|
|
176
|
+
config.noticed = true;
|
|
177
|
+
saveConfig(config);
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/** End the span with outcome */
|
|
181
|
+
async end({ status = 'completed', error_type = null, data = {} } = {}) {
|
|
182
|
+
if (disabled) return;
|
|
183
|
+
const duration_ms = Date.now() - startTime;
|
|
184
|
+
await send({
|
|
185
|
+
event: status === 'completed' ? 'command_completed' : 'command_failed',
|
|
186
|
+
run_id: runId,
|
|
187
|
+
timestamp: new Date().toISOString(),
|
|
188
|
+
env,
|
|
189
|
+
command: { name: command, args: positional, flags },
|
|
190
|
+
outcome: { status, duration_ms, error_type, data },
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Telemetry management (for `aw telemetry` command) ───────────────
|
|
197
|
+
|
|
198
|
+
export function enableTelemetry() {
|
|
199
|
+
const config = loadConfig();
|
|
200
|
+
config.enabled = true;
|
|
201
|
+
saveConfig(config);
|
|
202
|
+
return config;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function disableTelemetry() {
|
|
206
|
+
const config = loadConfig();
|
|
207
|
+
config.enabled = false;
|
|
208
|
+
saveConfig(config);
|
|
209
|
+
return config;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function getStatus() {
|
|
213
|
+
const config = loadConfig();
|
|
214
|
+
return {
|
|
215
|
+
enabled: !isDisabled(config),
|
|
216
|
+
machine_id: config.machine_id,
|
|
217
|
+
config_path: CONFIG_PATH,
|
|
218
|
+
};
|
|
219
|
+
}
|