@ghl-ai/aw 0.1.38-beta.0 → 0.1.38-beta.10
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 +25 -4
- package/codex.mjs +2 -15
- package/commands/init.mjs +7 -4
- package/commands/link-project.mjs +4 -0
- package/commands/nuke.mjs +19 -14
- package/commands/pull.mjs +1 -1
- package/commands/push-rules.mjs +2 -2
- package/commands/push.mjs +11 -4
- package/commands/slack-sim.mjs +128 -0
- package/commands/telemetry.mjs +87 -0
- package/config.mjs +1 -1
- package/constants.mjs +7 -0
- package/ecc.mjs +83 -4
- package/fmt.mjs +14 -0
- package/hooks/codex-home.mjs +3 -3
- package/hooks/shared-phase-scripts.mjs +17 -0
- package/hooks.mjs +106 -5
- package/package.json +8 -5
- package/slack-sim/fake-slack.mjs +200 -0
- package/slack-sim/http.mjs +170 -0
- package/slack-sim/in-process.mjs +263 -0
- package/slack-sim/render.mjs +42 -0
- package/slack-sim/scenario.mjs +64 -0
- package/slack-sim/scenarios/checkpoint-approve.json +21 -0
- package/slack-sim/scenarios/image-thread.json +27 -0
- package/slack-sim/scenarios/implementation-basic.json +18 -0
- package/slack-sim/scenarios/poll-webhook-race.json +18 -0
- package/slack-sim/scenarios/review-pr.json +14 -0
- package/telemetry.mjs +233 -0
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;
|
|
@@ -24,6 +25,7 @@ const COMMANDS = {
|
|
|
24
25
|
link: () => import('./commands/link-project.mjs').then(m => m.linkProjectCommand),
|
|
25
26
|
nuke: () => import('./commands/nuke.mjs').then(m => m.nukeCommand),
|
|
26
27
|
daemon: () => import('./commands/daemon.mjs').then(m => m.daemonCommand),
|
|
28
|
+
telemetry: () => import('./commands/telemetry.mjs').then(m => m.telemetryCommand),
|
|
27
29
|
};
|
|
28
30
|
|
|
29
31
|
function parseArgs(argv) {
|
|
@@ -109,6 +111,13 @@ function printHelp() {
|
|
|
109
111
|
cmd('aw daemon install --interval 30m', 'Set custom interval (e.g. 30m, 2h, 3600)'),
|
|
110
112
|
cmd('aw daemon uninstall', 'Stop the background daemon'),
|
|
111
113
|
cmd('aw daemon status', 'Check if daemon is running'),
|
|
114
|
+
cmd('aw slack-sim run <scenario>', 'Replay Slack-like scenarios against real runtime'),
|
|
115
|
+
cmd('aw slack-sim list-scenarios', 'List built-in Slack simulator scenarios'),
|
|
116
|
+
|
|
117
|
+
sec('Settings'),
|
|
118
|
+
cmd('aw telemetry status', 'Show telemetry status'),
|
|
119
|
+
cmd('aw telemetry disable', 'Opt out of anonymous analytics'),
|
|
120
|
+
cmd('aw telemetry enable', 'Re-enable analytics'),
|
|
112
121
|
|
|
113
122
|
sec('Examples'),
|
|
114
123
|
'',
|
|
@@ -158,9 +167,21 @@ export async function run(argv) {
|
|
|
158
167
|
}
|
|
159
168
|
|
|
160
169
|
if (command && COMMANDS[command]) {
|
|
170
|
+
const span = await startSpan(command, args);
|
|
171
|
+
span.notice();
|
|
161
172
|
args._updateCheck = updateCheck;
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
try {
|
|
174
|
+
const handler = await COMMANDS[command]();
|
|
175
|
+
await handler(args);
|
|
176
|
+
await span.end({ status: 'completed' });
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (err instanceof CancelError) {
|
|
179
|
+
await span.end({ status: 'cancelled', error_type: 'CancelError' });
|
|
180
|
+
process.exit(err.exitCode ?? 1);
|
|
181
|
+
}
|
|
182
|
+
await span.end({ status: 'failed', error_type: err.constructor.name });
|
|
183
|
+
throw err;
|
|
184
|
+
}
|
|
164
185
|
notifyUpdate(await updateCheck);
|
|
165
186
|
return;
|
|
166
187
|
}
|
|
@@ -170,5 +191,5 @@ export async function run(argv) {
|
|
|
170
191
|
process.exit(0);
|
|
171
192
|
}
|
|
172
193
|
|
|
173
|
-
fmt.
|
|
194
|
+
fmt.cancelAndExit(`Unknown command: ${command}`);
|
|
174
195
|
}
|
package/codex.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// codex.mjs — Project-local Codex defaults for AW/ECC startup.
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
|
|
6
6
|
import { getSupportedHarnessPhaseEntries } from './hook-manifest.mjs';
|
|
@@ -750,10 +750,6 @@ function ensureManagedScript(filePath, content, marker) {
|
|
|
750
750
|
if (existsSync(filePath)) {
|
|
751
751
|
const existing = readFileSync(filePath, 'utf8');
|
|
752
752
|
if (existing === content) {
|
|
753
|
-
if (!isExecutableScript(filePath)) {
|
|
754
|
-
chmodSync(filePath, 0o755);
|
|
755
|
-
return true;
|
|
756
|
-
}
|
|
757
753
|
return false;
|
|
758
754
|
}
|
|
759
755
|
if (!existing.includes(marker)) {
|
|
@@ -762,8 +758,7 @@ function ensureManagedScript(filePath, content, marker) {
|
|
|
762
758
|
}
|
|
763
759
|
|
|
764
760
|
mkdirSync(dirname(filePath), { recursive: true });
|
|
765
|
-
writeFileSync(filePath, content
|
|
766
|
-
chmodSync(filePath, 0o755);
|
|
761
|
+
writeFileSync(filePath, content);
|
|
767
762
|
return true;
|
|
768
763
|
}
|
|
769
764
|
|
|
@@ -786,14 +781,6 @@ function hasTomlSection(content, sectionName) {
|
|
|
786
781
|
return pattern.test(content);
|
|
787
782
|
}
|
|
788
783
|
|
|
789
|
-
function isExecutableScript(filePath) {
|
|
790
|
-
try {
|
|
791
|
-
return (statSync(filePath).mode & 0o111) !== 0;
|
|
792
|
-
} catch {
|
|
793
|
-
return false;
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
784
|
function tidyToml(content) {
|
|
798
785
|
const normalized = content
|
|
799
786
|
.replace(/\r\n/g, '\n')
|
package/commands/init.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
26
26
|
import { generateCommands, copyInstructions, initAwDocs, syncHomeHarnessInstructions } from '../integrate.mjs';
|
|
27
27
|
import { setupMcp } from '../mcp.mjs';
|
|
28
28
|
import { applyStoredStartupPreferences, ensureAwRuntimeHook } from '../startup.mjs';
|
|
29
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
29
30
|
import { autoUpdate, promptUpdate } from '../update.mjs';
|
|
30
31
|
import { installGlobalHooks } from '../hooks.mjs';
|
|
31
32
|
import { installAwEcc } from '../ecc.mjs';
|
|
@@ -100,7 +101,7 @@ function installIdeTasks() {
|
|
|
100
101
|
{
|
|
101
102
|
label: 'aw: sync registry',
|
|
102
103
|
type: 'shell',
|
|
103
|
-
command: 'aw init --silent',
|
|
104
|
+
command: 'AW_TRIGGER=ide:task aw init --silent',
|
|
104
105
|
presentation: { reveal: 'silent', panel: 'shared', close: true },
|
|
105
106
|
runOptions: { runOn: 'folderOpen' },
|
|
106
107
|
problemMatcher: [],
|
|
@@ -221,7 +222,7 @@ export async function initCommand(args) {
|
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
if (choice === 'platform-only') {
|
|
224
|
-
namespace =
|
|
225
|
+
namespace = 'platform'; team = 'platform'; subTeam = null; folderName = null;
|
|
225
226
|
}
|
|
226
227
|
}
|
|
227
228
|
|
|
@@ -310,6 +311,7 @@ export async function initCommand(args) {
|
|
|
310
311
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
311
312
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
312
313
|
const commands = generateCommands(HOME, { silent: true });
|
|
314
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
313
315
|
|
|
314
316
|
if (silent) {
|
|
315
317
|
autoUpdate(await args._updateCheck);
|
|
@@ -397,8 +399,8 @@ export async function initCommand(args) {
|
|
|
397
399
|
}
|
|
398
400
|
}
|
|
399
401
|
|
|
400
|
-
// Create sync config
|
|
401
|
-
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team, user });
|
|
402
|
+
// Create sync config — default to 'platform' when no namespace specified
|
|
403
|
+
const cfg = config.create(GLOBAL_AW_DIR, { namespace: team || 'platform', user });
|
|
402
404
|
if (folderName) {
|
|
403
405
|
config.addPattern(GLOBAL_AW_DIR, folderName);
|
|
404
406
|
}
|
|
@@ -445,6 +447,7 @@ export async function initCommand(args) {
|
|
|
445
447
|
const projectRegistryDir = cwd !== HOME ? join(cwd, '.aw', REGISTRY_DIR) : null;
|
|
446
448
|
const awDirForLinks = (projectRegistryDir && existsSync(projectRegistryDir)) ? projectRegistryDir : null;
|
|
447
449
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
450
|
+
if (cwd !== HOME) installLocalCommitHook(cwd);
|
|
448
451
|
ideSpinner.message('Generating commands...');
|
|
449
452
|
const commands = generateCommands(HOME, { silent: true });
|
|
450
453
|
ideSpinner.stop(`IDE wired — ${chalk.bold(symlinks)} symlinks · ${chalk.bold(commands)} commands`);
|
|
@@ -11,6 +11,7 @@ import { linkWorkspace } from '../link.mjs';
|
|
|
11
11
|
import { generateCommands } from '../integrate.mjs';
|
|
12
12
|
import { removeWorkspaceHookDefaults } from '../codex.mjs';
|
|
13
13
|
import { applyStoredStartupPreferences } from '../startup.mjs';
|
|
14
|
+
import { installLocalCommitHook } from '../hooks.mjs';
|
|
14
15
|
|
|
15
16
|
const HOME = homedir();
|
|
16
17
|
const AW_HOME = join(HOME, '.aw');
|
|
@@ -43,8 +44,10 @@ export function linkProjectCommand(args) {
|
|
|
43
44
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
44
45
|
const commands = generateCommands(HOME, { silent: true });
|
|
45
46
|
applyStoredStartupPreferences(HOME);
|
|
47
|
+
installLocalCommitHook(cwd);
|
|
46
48
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
47
49
|
fmt.logSuccess(`Already linked — refreshed ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands${removedLegacyStartupFiles.length > 0 ? ` · removed ${removedLegacyStartupFiles.length} legacy repo startup file${removedLegacyStartupFiles.length > 1 ? 's' : ''}` : ''}`);
|
|
50
|
+
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -55,6 +58,7 @@ export function linkProjectCommand(args) {
|
|
|
55
58
|
const symlinks = linkWorkspace(HOME, awDirForLinks, { silent: true });
|
|
56
59
|
const commands = generateCommands(HOME, { silent: true });
|
|
57
60
|
applyStoredStartupPreferences(HOME);
|
|
61
|
+
installLocalCommitHook(cwd);
|
|
58
62
|
const removedLegacyStartupFiles = removeWorkspaceHookDefaults(cwd);
|
|
59
63
|
fmt.logSuccess([
|
|
60
64
|
`Project linked — ${chalk.bold(symlinks)} IDE symlinks · ${chalk.bold(commands)} commands`,
|
package/commands/nuke.mjs
CHANGED
|
@@ -148,19 +148,24 @@ async function removeProjectSymlinks() {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// Also remove legacy local .git/hooks/post-checkout installed by old aw versions
|
|
151
|
+
// and prepare-commit-msg hooks installed by installLocalCommitHook
|
|
151
152
|
let hooksRemoved = 0;
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
{
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
153
|
+
const hookNames = ['post-checkout', 'prepare-commit-msg'];
|
|
154
|
+
for (const hookName of hookNames) {
|
|
155
|
+
const { stdout: hookFiles } = await exec(
|
|
156
|
+
`find "${HOME}" -maxdepth 5 -path "*/.git/hooks/${hookName}" -type f 2>/dev/null || true`,
|
|
157
|
+
{ encoding: 'utf8', timeout: 30000 }
|
|
158
|
+
);
|
|
159
|
+
for (const hookPath of hookFiles.trim().split('\n').filter(Boolean)) {
|
|
160
|
+
try {
|
|
161
|
+
const content = readFileSync(hookPath, 'utf8');
|
|
162
|
+
// Only remove hooks that AW installed — identified by our marker comment
|
|
163
|
+
if (content.includes('aw:') || content.includes('aw: auto-link registry') || content.includes('ln -s "$AW_REGISTRY"')) {
|
|
164
|
+
unlinkSync(hookPath);
|
|
165
|
+
hooksRemoved++;
|
|
166
|
+
}
|
|
167
|
+
} catch { /* best effort */ }
|
|
168
|
+
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
return { removed, hooksRemoved };
|
|
@@ -209,8 +214,8 @@ function removeIdeTasks() {
|
|
|
209
214
|
|
|
210
215
|
export async function nukeCommand(args) {
|
|
211
216
|
// Catch unhandled errors and surface them instead of letting clack show generic "Something went wrong"
|
|
212
|
-
process.on('uncaughtException', (e) => { fmt.
|
|
213
|
-
process.on('unhandledRejection', (e) => { fmt.
|
|
217
|
+
process.on('uncaughtException', (e) => { fmt.cancelAndExit(`Unexpected error: ${e.message}`); });
|
|
218
|
+
process.on('unhandledRejection', (e) => { fmt.cancelAndExit(`Unexpected error: ${e?.message ?? e}`); });
|
|
214
219
|
|
|
215
220
|
fmt.intro('aw nuke');
|
|
216
221
|
|
package/commands/pull.mjs
CHANGED
|
@@ -44,7 +44,7 @@ export async function pullCommand(args) {
|
|
|
44
44
|
const silent = args['--silent'] === true || args._silent === true;
|
|
45
45
|
|
|
46
46
|
const log = {
|
|
47
|
-
cancel: silent ? () => {
|
|
47
|
+
cancel: silent ? (msg) => { throw new fmt.CancelError(msg || 'silent cancel', { exitCode: 0 }); } : fmt.cancel,
|
|
48
48
|
logInfo: silent ? () => {} : fmt.logInfo,
|
|
49
49
|
logSuccess: silent ? () => {} : fmt.logSuccess,
|
|
50
50
|
logStep: silent ? () => {} : fmt.logStep,
|
package/commands/push-rules.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
|
|
|
5
5
|
|
|
6
6
|
import * as fmt from '../fmt.mjs';
|
|
7
7
|
import { chalk } from '../fmt.mjs';
|
|
8
|
-
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR } from '../constants.mjs';
|
|
8
|
+
import { REGISTRY_BASE_BRANCH, REGISTRY_REPO, RULES_SOURCE_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
9
9
|
import { syncFileTree } from '../file-tree.mjs';
|
|
10
10
|
|
|
11
11
|
function normalizeSlashes(path) {
|
|
@@ -147,7 +147,7 @@ function pushRulesTree(sourceRoot, { repo, dryRun, cwd }) {
|
|
|
147
147
|
const prTitle = buildRulesPrTitle(sourceRoot, cwd);
|
|
148
148
|
const prBody = buildRulesPrBody(sourceRoot, sourceType, cwd);
|
|
149
149
|
|
|
150
|
-
execSync(
|
|
150
|
+
execSync(`git commit -m "registry: sync platform rules\n\n${AW_CO_AUTHOR}"`, { cwd: tempDir, stdio: 'pipe' });
|
|
151
151
|
s2.stop('Rules sync prepared');
|
|
152
152
|
|
|
153
153
|
const s3 = fmt.spinner();
|
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';
|
|
@@ -10,7 +11,7 @@ const exec = promisify(execCb);
|
|
|
10
11
|
const execFile = promisify(execFileCb);
|
|
11
12
|
import * as fmt from '../fmt.mjs';
|
|
12
13
|
import { chalk } from '../fmt.mjs';
|
|
13
|
-
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR } from '../constants.mjs';
|
|
14
|
+
import { REGISTRY_REPO, REGISTRY_URL, REGISTRY_BASE_BRANCH, REGISTRY_DIR, AW_CO_AUTHOR } from '../constants.mjs';
|
|
14
15
|
import { resolveInput } from '../paths.mjs';
|
|
15
16
|
import { walkRegistryTree } from '../registry.mjs';
|
|
16
17
|
import {
|
|
@@ -25,6 +26,9 @@ import {
|
|
|
25
26
|
} from '../git.mjs';
|
|
26
27
|
import { hasRulesChanges, isRulesPushInput, pushRulesCommand } from './push-rules.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', 'references'];
|
|
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}\n${AW_CO_AUTHOR}`;
|
|
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,128 @@
|
|
|
1
|
+
import * as fmt from '../fmt.mjs';
|
|
2
|
+
import { loadScenario, listBuiltInScenarios } from '../slack-sim/scenario.mjs';
|
|
3
|
+
import { printDryRun, printScenarioIntro, printState } from '../slack-sim/render.mjs';
|
|
4
|
+
|
|
5
|
+
function sleep(ms) {
|
|
6
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function slackSimCommand(args) {
|
|
10
|
+
const [subcommand = 'list-scenarios', scenarioRef] = args._positional;
|
|
11
|
+
|
|
12
|
+
if (args['--help']) {
|
|
13
|
+
printHelp();
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (subcommand === 'list-scenarios') {
|
|
18
|
+
fmt.intro('aw slack-sim list-scenarios');
|
|
19
|
+
const scenarios = listBuiltInScenarios();
|
|
20
|
+
fmt.note(
|
|
21
|
+
scenarios.map((s) => `${s.name} ${fmt.chalk.dim(s.path)}`).join('\n'),
|
|
22
|
+
'Built-in scenarios',
|
|
23
|
+
);
|
|
24
|
+
fmt.outro('Done');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!['run', 'replay'].includes(subcommand)) {
|
|
29
|
+
fmt.cancel(`Unknown slack-sim subcommand: ${subcommand}`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const scenario = loadScenario(scenarioRef);
|
|
34
|
+
const useHttp = Boolean(args['--api-base']) && !args['--in-process'];
|
|
35
|
+
|
|
36
|
+
printScenarioIntro(scenario, useHttp ? 'http' : 'in-process');
|
|
37
|
+
fmt.note(
|
|
38
|
+
[
|
|
39
|
+
`source: ${scenario.__file}`,
|
|
40
|
+
`actions: ${scenario.actions.length}`,
|
|
41
|
+
`org: ${scenario.session.orgId}`,
|
|
42
|
+
`team: ${scenario.session.teamId}`,
|
|
43
|
+
].join('\n'),
|
|
44
|
+
'Scenario',
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
if (args['--dry-run']) {
|
|
48
|
+
printDryRun(scenario);
|
|
49
|
+
fmt.outro('Dry run complete');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const transport = useHttp
|
|
54
|
+
? await (await import('../slack-sim/http.mjs')).createHttpTransport({
|
|
55
|
+
apiBase: args['--api-base'],
|
|
56
|
+
orgId: scenario.session.orgId,
|
|
57
|
+
teamId: scenario.session.teamId,
|
|
58
|
+
})
|
|
59
|
+
: await (await import('../slack-sim/in-process.mjs')).createInProcessTransport();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await transport.start?.();
|
|
63
|
+
let currentChannel = scenario.defaults.channel;
|
|
64
|
+
let currentThreadTs = scenario.defaults.threadTs;
|
|
65
|
+
for (let i = 0; i < scenario.actions.length; i++) {
|
|
66
|
+
const action = scenario.actions[i];
|
|
67
|
+
const actionResult = await transport.runAction(action, scenario);
|
|
68
|
+
currentChannel = actionResult?.channel || action.channel || currentChannel;
|
|
69
|
+
currentThreadTs = actionResult?.threadTs || action.threadTs || action.thread_ts || (action.type === 'app_mention_top_level' ? actionResult?.ts : currentThreadTs);
|
|
70
|
+
const state = await transport.getState({
|
|
71
|
+
channel: currentChannel,
|
|
72
|
+
threadTs: currentThreadTs,
|
|
73
|
+
});
|
|
74
|
+
if (action.type === 'assert') {
|
|
75
|
+
assertState(state, action);
|
|
76
|
+
fmt.logSuccess(`Assertion ${i + 1} passed`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
printState(i, action, state);
|
|
80
|
+
if (args['--wait-for-finish'] && state.status === 'running' && action.type !== 'wait') {
|
|
81
|
+
await sleep(250);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
fmt.outro('Scenario complete');
|
|
85
|
+
} finally {
|
|
86
|
+
await transport.close?.();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertState(state, action) {
|
|
91
|
+
const failures = [];
|
|
92
|
+
if (action.status && state.status !== action.status) failures.push(`expected status=${action.status}, got ${state.status}`);
|
|
93
|
+
if (action.prRole && state.prRole !== action.prRole) failures.push(`expected prRole=${action.prRole}, got ${state.prRole}`);
|
|
94
|
+
if (action.pollerActive !== undefined && state.pollerActive !== action.pollerActive) {
|
|
95
|
+
failures.push(`expected pollerActive=${action.pollerActive}, got ${state.pollerActive}`);
|
|
96
|
+
}
|
|
97
|
+
if (action.checkpointActive !== undefined && state.checkpointActive !== action.checkpointActive) {
|
|
98
|
+
failures.push(`expected checkpointActive=${action.checkpointActive}, got ${state.checkpointActive}`);
|
|
99
|
+
}
|
|
100
|
+
if (action.prUrlIncludes && !String(state.prUrl || '').includes(action.prUrlIncludes)) {
|
|
101
|
+
failures.push(`expected prUrl to include "${action.prUrlIncludes}", got ${state.prUrl || '<none>'}`);
|
|
102
|
+
}
|
|
103
|
+
if (action.threadContains) {
|
|
104
|
+
const haystack = (state.messages || []).map((m) => m.text).join('\n');
|
|
105
|
+
if (!haystack.includes(action.threadContains)) failures.push(`expected thread to contain "${action.threadContains}"`);
|
|
106
|
+
}
|
|
107
|
+
if (action.threadNotContains) {
|
|
108
|
+
const haystack = (state.messages || []).map((m) => m.text).join('\n');
|
|
109
|
+
if (haystack.includes(action.threadNotContains)) failures.push(`expected thread to not contain "${action.threadNotContains}"`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (failures.length) {
|
|
113
|
+
throw new Error(failures.join('; '));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function printHelp() {
|
|
118
|
+
fmt.intro('aw slack-sim');
|
|
119
|
+
fmt.note(
|
|
120
|
+
[
|
|
121
|
+
'aw slack-sim list-scenarios',
|
|
122
|
+
'aw slack-sim run <scenario-file-or-name> [--dry-run] [--in-process] [--api-base <url>]',
|
|
123
|
+
'aw slack-sim replay <scenario-file-or-name> [--dry-run] [--in-process] [--api-base <url>]',
|
|
124
|
+
].join('\n'),
|
|
125
|
+
'Usage',
|
|
126
|
+
);
|
|
127
|
+
fmt.outro('Done');
|
|
128
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// commands/telemetry.mjs — `aw telemetry [enable|disable|status|flush-queue]`
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { enableTelemetry, disableTelemetry, getStatus } from '../telemetry.mjs';
|
|
7
|
+
import * as fmt from '../fmt.mjs';
|
|
8
|
+
import { chalk } from '../fmt.mjs';
|
|
9
|
+
|
|
10
|
+
export async function telemetryCommand(args) {
|
|
11
|
+
const sub = args._positional?.[0];
|
|
12
|
+
|
|
13
|
+
if (sub === 'disable') {
|
|
14
|
+
disableTelemetry();
|
|
15
|
+
fmt.logSuccess('Telemetry disabled. No anonymous usage data will be sent.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (sub === 'enable') {
|
|
20
|
+
enableTelemetry();
|
|
21
|
+
fmt.logSuccess('Telemetry enabled. Anonymous usage stats help improve aw.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (sub === 'flush-queue') {
|
|
26
|
+
await flushQueueCommand();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// status (default)
|
|
31
|
+
const status = getStatus();
|
|
32
|
+
fmt.intro('aw telemetry');
|
|
33
|
+
fmt.logStep(`Status: ${status.enabled ? chalk.green('enabled') : chalk.red('disabled')}`);
|
|
34
|
+
fmt.logStep(`Machine ID: ${chalk.dim(status.machine_id)}`);
|
|
35
|
+
fmt.logStep(`Config: ${chalk.dim(status.config_path)}`);
|
|
36
|
+
|
|
37
|
+
// Show queue depth if available
|
|
38
|
+
const queueFile = join(homedir(), '.aw', 'telemetry', 'queue.jsonl');
|
|
39
|
+
if (existsSync(queueFile)) {
|
|
40
|
+
try {
|
|
41
|
+
const lines = readFileSync(queueFile, 'utf8').trim().split('\n').filter(Boolean);
|
|
42
|
+
fmt.logStep(`Queue: ${chalk.yellow(lines.length)} pending prompt(s)`);
|
|
43
|
+
} catch { /* best effort */ }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fmt.logMessage('');
|
|
47
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry disable')} — opt out of anonymous analytics`);
|
|
48
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry enable')} — re-enable analytics`);
|
|
49
|
+
fmt.logMessage(` ${chalk.dim('aw telemetry flush-queue')} — manually flush pending queue`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function flushQueueCommand() {
|
|
53
|
+
const eccBase = join(homedir(), '.aw-ecc');
|
|
54
|
+
const libPath = join(eccBase, 'scripts', 'hooks', 'capabilities', 'telemetry', 'telemetry-lib.js');
|
|
55
|
+
|
|
56
|
+
if (!existsSync(libPath)) {
|
|
57
|
+
fmt.logWarn('Telemetry library not found. Run aw init first.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const {
|
|
62
|
+
readQueue,
|
|
63
|
+
flushQueueToApi,
|
|
64
|
+
getNamespace,
|
|
65
|
+
buildTelemetryHeaders,
|
|
66
|
+
} = await import(libPath);
|
|
67
|
+
|
|
68
|
+
const entries = readQueue();
|
|
69
|
+
if (entries.length === 0) {
|
|
70
|
+
fmt.logSuccess('Queue is empty — nothing to flush.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fmt.logStep(`Flushing ${entries.length} pending prompt(s)...`);
|
|
75
|
+
|
|
76
|
+
const namespace = getNamespace();
|
|
77
|
+
const headers = buildTelemetryHeaders(namespace);
|
|
78
|
+
const result = await flushQueueToApi(headers);
|
|
79
|
+
|
|
80
|
+
if (result.flushed > 0) {
|
|
81
|
+
fmt.logSuccess(`Flushed ${result.flushed} prompt(s) to API.`);
|
|
82
|
+
} else if (result.failed) {
|
|
83
|
+
fmt.logWarn('Flush failed — entries remain in queue for retry.');
|
|
84
|
+
} else {
|
|
85
|
+
fmt.logSuccess('Nothing to flush.');
|
|
86
|
+
}
|
|
87
|
+
}
|
package/config.mjs
CHANGED
package/constants.mjs
CHANGED
|
@@ -27,3 +27,10 @@ export const AW_HOME = join(homedir(), '.aw');
|
|
|
27
27
|
|
|
28
28
|
/** Directory in platform-docs repo containing platform rules (pulled into .aw_registry/.aw_rules/) */
|
|
29
29
|
export const RULES_SOURCE_DIR = '.aw_rules';
|
|
30
|
+
/** Telemetry endpoint — override with AW_TELEMETRY_URL env var */
|
|
31
|
+
export const TELEMETRY_URL = process.env.AW_TELEMETRY_URL || 'https://services.leadconnectorhq.com/agentic-workspace/api/telemetry/events';
|
|
32
|
+
|
|
33
|
+
/** AW bot identity for Co-Authored-By trailers */
|
|
34
|
+
export const AW_BOT_NAME = 'AW';
|
|
35
|
+
export const AW_BOT_EMAIL = process.env.AW_BOT_EMAIL || '273421570+ghl-aw@users.noreply.github.com';
|
|
36
|
+
export const AW_CO_AUTHOR = `Co-Authored-By: ${AW_BOT_NAME} <${AW_BOT_EMAIL}>`;
|