@ai-outfitter/outfitter 1.7.1 → 1.8.1
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/code/pi-extension/src/outfitter-extension.js +7 -1
- package/dist/cli/commands/RunAgentCommand.js +11 -2
- package/dist/cli/commands/RunAgentCommand.js.map +1 -1
- package/dist/cli.d.ts +12 -1
- package/dist/cli.js +92 -1
- package/dist/cli.js.map +1 -1
- package/dist/paths/OutfitterCache.d.ts +2 -0
- package/dist/paths/OutfitterCache.js +8 -0
- package/dist/paths/OutfitterCache.js.map +1 -1
- package/dist/projection/ProjectHarness.js +10 -8
- package/dist/projection/ProjectHarness.js.map +1 -1
- package/dist/schemas/settings.schema.json +11 -0
- package/dist/settings/Settings.d.ts +4 -0
- package/dist/settings/Settings.js.map +1 -1
- package/dist/settings/SettingsLoader.js +4 -1
- package/dist/settings/SettingsLoader.js.map +1 -1
- package/dist/settings/SettingsMerger.js +3 -0
- package/dist/settings/SettingsMerger.js.map +1 -1
- package/dist/setup/Setup.d.ts +1 -1
- package/dist/setup/Setup.js +16 -6
- package/dist/setup/Setup.js.map +1 -1
- package/dist/telemetry/CiEnvironment.d.ts +6 -0
- package/dist/telemetry/CiEnvironment.js +4 -0
- package/dist/telemetry/CiEnvironment.js.map +1 -0
- package/dist/telemetry/TelemetryConsent.d.ts +8 -0
- package/dist/telemetry/TelemetryConsent.js +39 -0
- package/dist/telemetry/TelemetryConsent.js.map +1 -0
- package/dist/telemetry/TelemetryConstants.d.ts +10 -0
- package/dist/telemetry/TelemetryConstants.js +11 -0
- package/dist/telemetry/TelemetryConstants.js.map +1 -0
- package/dist/telemetry/TelemetryContext.d.ts +15 -0
- package/dist/telemetry/TelemetryContext.js +13 -0
- package/dist/telemetry/TelemetryContext.js.map +1 -0
- package/dist/telemetry/TelemetryService.d.ts +57 -0
- package/dist/telemetry/TelemetryService.js +157 -0
- package/dist/telemetry/TelemetryService.js.map +1 -0
- package/dist/telemetry/TelemetryState.d.ts +11 -0
- package/dist/telemetry/TelemetryState.js +51 -0
- package/dist/telemetry/TelemetryState.js.map +1 -0
- package/dist/version/OutfitterVersion.js +6 -3
- package/dist/version/OutfitterVersion.js.map +1 -1
- package/docs/documentation/README.md +2 -0
- package/docs/documentation/channels.md +112 -0
- package/docs/documentation/cli.md +9 -0
- package/docs/documentation/settings.md +5 -0
- package/docs/documentation/support-matrix.md +3 -2
- package/docs/documentation/telemetry.md +72 -0
- package/package.json +4 -2
- package/src/schemas/settings.schema.json +11 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CiEnvironment.js","sourceRoot":"","sources":["../../src/telemetry/CiEnvironment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAOnC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,QAAuB,EAAiB,EAAE,CAC5E,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;AAEpD,MAAM,CAAC,MAAM,QAAQ,GAAG,GAAe,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,mBAAmB,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SettingsLoadResult } from '../settings/SettingsLoader.js';
|
|
2
|
+
export type TelemetryConsentSource = 'default' | 'user settings' | 'user-local settings' | 'project settings' | 'project-local settings' | 'invalid settings' | 'OUTFITTER_TELEMETRY' | 'DO_NOT_TRACK';
|
|
3
|
+
export interface TelemetryConsent {
|
|
4
|
+
readonly enabled: boolean;
|
|
5
|
+
readonly source: TelemetryConsentSource;
|
|
6
|
+
}
|
|
7
|
+
export type TelemetryEnvironment = Readonly<Record<string, string | undefined>>;
|
|
8
|
+
export declare const resolveTelemetryConsent: (loaded: SettingsLoadResult, env: TelemetryEnvironment) => TelemetryConsent;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const localSource = (file) => {
|
|
2
|
+
switch (file.location.scope) {
|
|
3
|
+
case 'user':
|
|
4
|
+
return 'user settings';
|
|
5
|
+
case 'user-local':
|
|
6
|
+
return 'user-local settings';
|
|
7
|
+
case 'project':
|
|
8
|
+
return 'project settings';
|
|
9
|
+
case 'project-local':
|
|
10
|
+
return 'project-local settings';
|
|
11
|
+
case 'remote':
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const environmentConsent = (env) => {
|
|
16
|
+
if (env.OUTFITTER_TELEMETRY === '0')
|
|
17
|
+
return { enabled: false, source: 'OUTFITTER_TELEMETRY' };
|
|
18
|
+
if (env.DO_NOT_TRACK === '1')
|
|
19
|
+
return { enabled: false, source: 'DO_NOT_TRACK' };
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
export const resolveTelemetryConsent = (loaded, env) => {
|
|
23
|
+
const killed = environmentConsent(env);
|
|
24
|
+
if (killed !== undefined)
|
|
25
|
+
return killed;
|
|
26
|
+
if (loaded.issues.length > 0)
|
|
27
|
+
return { enabled: false, source: 'invalid settings' };
|
|
28
|
+
// Any local opt-out wins. Remote settings are deliberately excluded from consent.
|
|
29
|
+
const disabled = loaded.files.findLast((file) => localSource(file) !== undefined && file.settings.telemetry?.enabled === false);
|
|
30
|
+
if (disabled !== undefined)
|
|
31
|
+
return { enabled: false, source: localSource(disabled) };
|
|
32
|
+
// Only settings owned by the user may explicitly enable collection.
|
|
33
|
+
const enabled = loaded.files.findLast((file) => (file.location.scope === 'user' || file.location.scope === 'user-local') &&
|
|
34
|
+
file.settings.telemetry?.enabled === true);
|
|
35
|
+
if (enabled !== undefined)
|
|
36
|
+
return { enabled: true, source: localSource(enabled) };
|
|
37
|
+
return { enabled: true, source: 'default' };
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=TelemetryConsent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryConsent.js","sourceRoot":"","sources":["../../src/telemetry/TelemetryConsent.ts"],"names":[],"mappings":"AAmBA,MAAM,WAAW,GAAG,CAAC,IAAwB,EAAsC,EAAE;IACnF,QAAQ,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QAC5B,KAAK,MAAM;YACT,OAAO,eAAe,CAAC;QACzB,KAAK,YAAY;YACf,OAAO,qBAAqB,CAAC;QAC/B,KAAK,SAAS;YACZ,OAAO,kBAAkB,CAAC;QAC5B,KAAK,eAAe;YAClB,OAAO,wBAAwB,CAAC;QAClC,KAAK,QAAQ;YACX,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,kBAAkB,GAAG,CAAC,GAAyB,EAAgC,EAAE;IACrF,IAAI,GAAG,CAAC,mBAAmB,KAAK,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAC9F,IAAI,GAAG,CAAC,YAAY,KAAK,GAAG;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IAChF,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,MAA0B,EAAE,GAAyB,EAAoB,EAAE;IACjH,MAAM,MAAM,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAEpF,kFAAkF;IAClF,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CACpC,CAAC,IAAI,EAAE,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,SAAS,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,KAAK,KAAK,CACxF,CAAC;IACF,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAE,EAAE,CAAC;IAEtF,oEAAoE;IACpE,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,CACnC,CAAC,IAAI,EAAE,EAAE,CACP,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,KAAK,YAAY,CAAC;QACxE,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAC5C,CAAC;IACF,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,OAAO,CAAE,EAAE,CAAC;IAEnF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC9C,CAAC,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog project configuration. An empty key makes telemetry completely inert.
|
|
3
|
+
*
|
|
4
|
+
* This is a PostHog project API key: write-only, safe to ship in client code, and useless for
|
|
5
|
+
* reading any project data. It belongs to the `ai-outfitter` organization, project 562393, on
|
|
6
|
+
* PostHog Cloud US — so `POSTHOG_HOST` must stay a US endpoint or events are accepted nowhere.
|
|
7
|
+
*/
|
|
8
|
+
export declare const POSTHOG_API_KEY = "phc_v9FGDjtEC7h9UvLxHdJKaHFtFfMN7UZwpJ2weRTFoqvz";
|
|
9
|
+
export declare const POSTHOG_HOST = "https://us.i.posthog.com";
|
|
10
|
+
export declare const TELEMETRY_SHUTDOWN_BUDGET_MS = 1000;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostHog project configuration. An empty key makes telemetry completely inert.
|
|
3
|
+
*
|
|
4
|
+
* This is a PostHog project API key: write-only, safe to ship in client code, and useless for
|
|
5
|
+
* reading any project data. It belongs to the `ai-outfitter` organization, project 562393, on
|
|
6
|
+
* PostHog Cloud US — so `POSTHOG_HOST` must stay a US endpoint or events are accepted nowhere.
|
|
7
|
+
*/
|
|
8
|
+
export const POSTHOG_API_KEY = 'phc_v9FGDjtEC7h9UvLxHdJKaHFtFfMN7UZwpJ2weRTFoqvz';
|
|
9
|
+
export const POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
10
|
+
export const TELEMETRY_SHUTDOWN_BUDGET_MS = 1000;
|
|
11
|
+
//# sourceMappingURL=TelemetryConstants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryConstants.js","sourceRoot":"","sources":["../../src/telemetry/TelemetryConstants.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,kDAAkD,CAAC;AAClF,MAAM,CAAC,MAAM,YAAY,GAAG,0BAA0B,CAAC;AACvD,MAAM,CAAC,MAAM,4BAA4B,GAAG,IAAI,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { SettingsLoadResult } from '../settings/SettingsLoader.js';
|
|
2
|
+
import type { TelemetryEnvironment } from './TelemetryConsent.js';
|
|
3
|
+
import type { DetectedCi } from './CiEnvironment.js';
|
|
4
|
+
import type { TelemetryStateStore } from './TelemetryState.js';
|
|
5
|
+
export interface TelemetryContextInput {
|
|
6
|
+
readonly homeDirectory: string;
|
|
7
|
+
readonly projectDirectory: string;
|
|
8
|
+
readonly env: TelemetryEnvironment;
|
|
9
|
+
}
|
|
10
|
+
export interface TelemetryContext {
|
|
11
|
+
readonly settingsReader: () => SettingsLoadResult;
|
|
12
|
+
readonly stateStore: TelemetryStateStore;
|
|
13
|
+
readonly ci: DetectedCi;
|
|
14
|
+
}
|
|
15
|
+
export declare const createTelemetryContext: (input: TelemetryContextInput) => TelemetryContext;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Composes the process-level telemetry collaborators used by the CLI entrypoint.
|
|
2
|
+
import { discoverSettingsLoadPlan, loadSettingsFiles } from '../settings/SettingsLoader.js';
|
|
3
|
+
import { detectCi } from './CiEnvironment.js';
|
|
4
|
+
import { createTelemetryStateStore, resolveTelemetryStatePath } from './TelemetryState.js';
|
|
5
|
+
export const createTelemetryContext = (input) => {
|
|
6
|
+
const plan = discoverSettingsLoadPlan(input);
|
|
7
|
+
return {
|
|
8
|
+
settingsReader: () => loadSettingsFiles(plan),
|
|
9
|
+
stateStore: createTelemetryStateStore(resolveTelemetryStatePath(input.homeDirectory, input.env)),
|
|
10
|
+
ci: detectCi(),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=TelemetryContext.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryContext.js","sourceRoot":"","sources":["../../src/telemetry/TelemetryContext.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,OAAO,EAAE,wBAAwB,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAG5F,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,EAAE,yBAAyB,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAe3F,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAA4B,EAAoB,EAAE;IACvF,MAAM,IAAI,GAAG,wBAAwB,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO;QACL,cAAc,EAAE,GAAG,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC;QAC7C,UAAU,EAAE,yBAAyB,CAAC,yBAAyB,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAChG,EAAE,EAAE,QAAQ,EAAE;KACf,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { PostHogOptions } from 'posthog-node';
|
|
2
|
+
import type { SettingsLoadResult } from '../settings/SettingsLoader.js';
|
|
3
|
+
import type { TelemetryEnvironment } from './TelemetryConsent.js';
|
|
4
|
+
import type { TelemetryStateStore } from './TelemetryState.js';
|
|
5
|
+
import type { DetectedCi } from './CiEnvironment.js';
|
|
6
|
+
export type DurationBucket = '<1s' | '1-5s' | '5-30s' | '30s+';
|
|
7
|
+
export type WarningCountBucket = '0' | '1-5' | '5+' | 'unknown';
|
|
8
|
+
export type TelemetryOutcome = 'success' | 'error';
|
|
9
|
+
export interface TelemetryCommandContext {
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly outfitterVersion: string;
|
|
12
|
+
readonly nodeVersion: string;
|
|
13
|
+
readonly platform: string;
|
|
14
|
+
readonly architecture: string;
|
|
15
|
+
readonly interactive: boolean;
|
|
16
|
+
readonly harness?: string;
|
|
17
|
+
readonly strict?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface TelemetryCompletionContext extends TelemetryCommandContext {
|
|
20
|
+
readonly outcome: TelemetryOutcome;
|
|
21
|
+
readonly durationMs: number;
|
|
22
|
+
readonly exitCode: number;
|
|
23
|
+
}
|
|
24
|
+
export interface TelemetryClient {
|
|
25
|
+
capture(message: {
|
|
26
|
+
readonly distinctId: string;
|
|
27
|
+
readonly event: string;
|
|
28
|
+
readonly properties: Record<string, unknown>;
|
|
29
|
+
}): void | Promise<void>;
|
|
30
|
+
shutdown(timeoutMs?: number): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
export interface TelemetryClientOptions {
|
|
33
|
+
readonly host: string;
|
|
34
|
+
readonly disableGeoip: true;
|
|
35
|
+
readonly fetch: TelemetryFetch;
|
|
36
|
+
}
|
|
37
|
+
export type TelemetryFetch = NonNullable<PostHogOptions['fetch']>;
|
|
38
|
+
export type TelemetryClientFactory = (apiKey: string, options: TelemetryClientOptions) => TelemetryClient | Promise<TelemetryClient>;
|
|
39
|
+
export interface TelemetryService {
|
|
40
|
+
captureCommandStarted(context: TelemetryCommandContext): Promise<void>;
|
|
41
|
+
captureCommandCompleted(context: TelemetryCompletionContext): Promise<void>;
|
|
42
|
+
shutdown(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
export interface TelemetryServiceDependencies {
|
|
45
|
+
readonly settingsReader: () => SettingsLoadResult;
|
|
46
|
+
readonly stateStore: TelemetryStateStore;
|
|
47
|
+
readonly env: TelemetryEnvironment;
|
|
48
|
+
readonly ci: DetectedCi;
|
|
49
|
+
readonly writeError: (message: string) => void;
|
|
50
|
+
readonly apiKey?: string;
|
|
51
|
+
readonly clientFactory?: TelemetryClientFactory;
|
|
52
|
+
readonly shutdownBudgetMs?: number;
|
|
53
|
+
}
|
|
54
|
+
export declare const createBoundedTelemetryFetch: (fetchImplementation: typeof globalThis.fetch, budgetMs: number) => TelemetryFetch;
|
|
55
|
+
export declare const buildCommandStartedProperties: (context: TelemetryCommandContext, ci: DetectedCi) => Record<string, unknown>;
|
|
56
|
+
export declare const buildCommandCompletedProperties: (context: TelemetryCompletionContext, ci: DetectedCi) => Record<string, unknown>;
|
|
57
|
+
export declare const createTelemetryService: (dependencies: TelemetryServiceDependencies) => TelemetryService;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { HARNESSES } from '../settings/Settings.js';
|
|
2
|
+
import { POSTHOG_API_KEY, POSTHOG_HOST, TELEMETRY_SHUTDOWN_BUDGET_MS } from './TelemetryConstants.js';
|
|
3
|
+
import { resolveTelemetryConsent } from './TelemetryConsent.js';
|
|
4
|
+
const OS_FAMILIES = ['aix', 'android', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32', 'unknown'];
|
|
5
|
+
const ARCHITECTURES = [
|
|
6
|
+
'arm',
|
|
7
|
+
'arm64',
|
|
8
|
+
'ia32',
|
|
9
|
+
'loong64',
|
|
10
|
+
'mips',
|
|
11
|
+
'mipsel',
|
|
12
|
+
'ppc',
|
|
13
|
+
'ppc64',
|
|
14
|
+
'riscv64',
|
|
15
|
+
's390',
|
|
16
|
+
's390x',
|
|
17
|
+
'x64',
|
|
18
|
+
'unknown',
|
|
19
|
+
];
|
|
20
|
+
const defaultClientFactory = async (apiKey, options) => {
|
|
21
|
+
const { PostHog } = await import('posthog-node');
|
|
22
|
+
return new PostHog(apiKey, options);
|
|
23
|
+
};
|
|
24
|
+
const syntheticSuccess = () => ({
|
|
25
|
+
status: 200,
|
|
26
|
+
text: () => Promise.resolve(''),
|
|
27
|
+
json: () => Promise.resolve({}),
|
|
28
|
+
headers: { get: () => null },
|
|
29
|
+
body: null,
|
|
30
|
+
});
|
|
31
|
+
export const createBoundedTelemetryFetch = (fetchImplementation, budgetMs) => async (url, options) => {
|
|
32
|
+
try {
|
|
33
|
+
// Leave time for the SDK to finish its queue drain before its own shutdown timer fires.
|
|
34
|
+
const requestBudgetMs = Math.max(1, budgetMs - Math.min(50, budgetMs / 2));
|
|
35
|
+
const signal = AbortSignal.timeout(requestBudgetMs);
|
|
36
|
+
const deadline = new Promise((resolve) => {
|
|
37
|
+
signal.addEventListener('abort', () => resolve(syntheticSuccess()), { once: true });
|
|
38
|
+
});
|
|
39
|
+
const request = fetchImplementation(url, { ...options, signal });
|
|
40
|
+
const response = await Promise.race([request, deadline]);
|
|
41
|
+
if (response.status >= 200 && response.status < 300)
|
|
42
|
+
return response;
|
|
43
|
+
void response.body?.cancel().catch(() => undefined);
|
|
44
|
+
return syntheticSuccess();
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return syntheticSuccess();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const durationBucket = (milliseconds) => {
|
|
51
|
+
if (milliseconds < 1000)
|
|
52
|
+
return '<1s';
|
|
53
|
+
if (milliseconds < 5000)
|
|
54
|
+
return '1-5s';
|
|
55
|
+
if (milliseconds < 30_000)
|
|
56
|
+
return '5-30s';
|
|
57
|
+
return '30s+';
|
|
58
|
+
};
|
|
59
|
+
const knownValue = (values, value) => values.includes(value) ? value : 'unknown';
|
|
60
|
+
export const buildCommandStartedProperties = (context, ci) => ({
|
|
61
|
+
command: context.command,
|
|
62
|
+
outfitter_version: context.outfitterVersion,
|
|
63
|
+
node_major: Number.parseInt(context.nodeVersion.split('.')[0], 10),
|
|
64
|
+
os_family: knownValue(OS_FAMILIES, context.platform),
|
|
65
|
+
arch: knownValue(ARCHITECTURES, context.architecture),
|
|
66
|
+
interactive: context.interactive,
|
|
67
|
+
harness: knownValue(HARNESSES, context.harness ?? 'unknown'),
|
|
68
|
+
strict: context.strict === true,
|
|
69
|
+
is_ci: ci.isCI,
|
|
70
|
+
ci_name: ci.isCI ? (ci.vendorId ?? 'unknown') : 'none',
|
|
71
|
+
$process_person_profile: false,
|
|
72
|
+
});
|
|
73
|
+
export const buildCommandCompletedProperties = (context, ci) => ({
|
|
74
|
+
...buildCommandStartedProperties(context, ci),
|
|
75
|
+
outcome: context.outcome,
|
|
76
|
+
duration_bucket: durationBucket(context.durationMs),
|
|
77
|
+
exit_code_class: context.exitCode === 0 ? 'success' : 'error',
|
|
78
|
+
warning_count_bucket: 'unknown',
|
|
79
|
+
});
|
|
80
|
+
const NOTICE = [
|
|
81
|
+
'Outfitter sends pseudonymous command adoption and reliability analytics',
|
|
82
|
+
'to PostHog. No content, paths, or free-form arguments are collected.',
|
|
83
|
+
"Opt out: set 'telemetry.enabled: false' in ~/.agents/settings.yml, or set",
|
|
84
|
+
'OUTFITTER_TELEMETRY=0 / DO_NOT_TRACK=1.',
|
|
85
|
+
'Details:',
|
|
86
|
+
'https://github.com/ai-outfitter/outfitter/blob/main/docs/documentation/telemetry.md',
|
|
87
|
+
].join('\n');
|
|
88
|
+
export const createTelemetryService = (dependencies) => {
|
|
89
|
+
/* v8 ignore next -- tests never consume a compiled production key; they inject an empty or test key. */
|
|
90
|
+
const apiKey = dependencies.apiKey ?? POSTHOG_API_KEY;
|
|
91
|
+
const shutdownBudgetMs = dependencies.shutdownBudgetMs ?? TELEMETRY_SHUTDOWN_BUDGET_MS;
|
|
92
|
+
const clientFactory = dependencies.clientFactory ?? defaultClientFactory;
|
|
93
|
+
const ci = dependencies.ci;
|
|
94
|
+
let client;
|
|
95
|
+
let distinctId;
|
|
96
|
+
let prepared;
|
|
97
|
+
// Consent, client, and state are resolved once per process.
|
|
98
|
+
const prepare = async () => {
|
|
99
|
+
if (prepared !== undefined)
|
|
100
|
+
return prepared;
|
|
101
|
+
prepared = false;
|
|
102
|
+
// An empty compiled key keeps telemetry fully inert: no client, no consent read, no state access.
|
|
103
|
+
if (apiKey === '')
|
|
104
|
+
return false;
|
|
105
|
+
const consent = resolveTelemetryConsent(dependencies.settingsReader(), dependencies.env);
|
|
106
|
+
if (!consent.enabled) {
|
|
107
|
+
if (!ci.isCI)
|
|
108
|
+
dependencies.stateStore.delete();
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
client = await clientFactory(apiKey, {
|
|
112
|
+
host: POSTHOG_HOST,
|
|
113
|
+
disableGeoip: true,
|
|
114
|
+
fetch: createBoundedTelemetryFetch(globalThis.fetch.bind(globalThis), shutdownBudgetMs),
|
|
115
|
+
});
|
|
116
|
+
if (ci.isCI) {
|
|
117
|
+
distinctId = `ci.${ci.vendorId ?? 'unknown'}`;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
const state = dependencies.stateStore.readOrCreate();
|
|
121
|
+
distinctId = state.installation_id;
|
|
122
|
+
if (!state.notice_shown) {
|
|
123
|
+
dependencies.stateStore.recordNoticeShown(state);
|
|
124
|
+
dependencies.writeError(NOTICE);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
prepared = true;
|
|
128
|
+
return true;
|
|
129
|
+
};
|
|
130
|
+
const capture = async (event, properties) => {
|
|
131
|
+
try {
|
|
132
|
+
if (!(await prepare()))
|
|
133
|
+
return;
|
|
134
|
+
const result = client.capture({ distinctId: distinctId, event, properties });
|
|
135
|
+
// The SDK queues synchronously. A non-standard client promise is observed but never allowed to delay the CLI.
|
|
136
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Analytics must never affect command behavior or emit diagnostics.
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
return {
|
|
143
|
+
captureCommandStarted: (context) => capture('cli command started', buildCommandStartedProperties(context, ci)),
|
|
144
|
+
captureCommandCompleted: (context) => capture('cli command completed', buildCommandCompletedProperties(context, ci)),
|
|
145
|
+
async shutdown() {
|
|
146
|
+
if (client === undefined)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
await client.shutdown(shutdownBudgetMs);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Analytics must never affect command behavior or emit diagnostics.
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
//# sourceMappingURL=TelemetryService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryService.js","sourceRoot":"","sources":["../../src/telemetry/TelemetryService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface TelemetryState {
|
|
2
|
+
readonly installation_id: string;
|
|
3
|
+
readonly notice_shown: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface TelemetryStateStore {
|
|
6
|
+
readOrCreate(): TelemetryState;
|
|
7
|
+
recordNoticeShown(state: TelemetryState): TelemetryState;
|
|
8
|
+
delete(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare const resolveTelemetryStatePath: (homeDirectory: string, env: Readonly<Record<string, string | undefined>>) => string;
|
|
11
|
+
export declare const createTelemetryStateStore: (path: string, createId?: () => string) => TelemetryStateStore;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { resolveOutfitterStateDir } from '../paths/OutfitterCache.js';
|
|
5
|
+
export const resolveTelemetryStatePath = (homeDirectory, env) => join(resolveOutfitterStateDir(env, homeDirectory), 'telemetry.json');
|
|
6
|
+
const isTelemetryState = (value) => {
|
|
7
|
+
if (value === null || typeof value !== 'object')
|
|
8
|
+
return false;
|
|
9
|
+
const candidate = value;
|
|
10
|
+
return typeof candidate.installation_id === 'string' && typeof candidate.notice_shown === 'boolean';
|
|
11
|
+
};
|
|
12
|
+
export const createTelemetryStateStore = (path, createId = randomUUID) => {
|
|
13
|
+
const write = (state) => {
|
|
14
|
+
const directory = dirname(path);
|
|
15
|
+
const temporaryPath = join(directory, `.telemetry-${randomUUID()}.tmp`);
|
|
16
|
+
mkdirSync(directory, { recursive: true });
|
|
17
|
+
try {
|
|
18
|
+
writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
19
|
+
renameSync(temporaryPath, path);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
rmSync(temporaryPath, { force: true });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
readOrCreate() {
|
|
27
|
+
if (existsSync(path)) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
30
|
+
if (isTelemetryState(parsed))
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A partial or corrupt state file is replaced with a fresh pseudonymous identifier.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const state = { installation_id: createId(), notice_shown: false };
|
|
38
|
+
write(state);
|
|
39
|
+
return state;
|
|
40
|
+
},
|
|
41
|
+
recordNoticeShown(state) {
|
|
42
|
+
const updated = { ...state, notice_shown: true };
|
|
43
|
+
write(updated);
|
|
44
|
+
return updated;
|
|
45
|
+
},
|
|
46
|
+
delete() {
|
|
47
|
+
rmSync(path, { force: true });
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=TelemetryState.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TelemetryState.js","sourceRoot":"","sources":["../../src/telemetry/TelemetryState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACjG,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAE1C,OAAO,EAAE,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AAatE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CACvC,aAAqB,EACrB,GAAiD,EACzC,EAAE,CAAC,IAAI,CAAC,wBAAwB,CAAC,GAAG,EAAE,aAAa,CAAC,EAAE,gBAAgB,CAAC,CAAC;AAElF,MAAM,gBAAgB,GAAG,CAAC,KAAc,EAA2B,EAAE;IACnE,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC9D,MAAM,SAAS,GAAG,KAAgC,CAAC;IACnD,OAAO,OAAO,SAAS,CAAC,eAAe,KAAK,QAAQ,IAAI,OAAO,SAAS,CAAC,YAAY,KAAK,SAAS,CAAC;AACtG,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,IAAY,EAAE,WAAyB,UAAU,EAAuB,EAAE;IAClH,MAAM,KAAK,GAAG,CAAC,KAAqB,EAAQ,EAAE;QAC5C,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,EAAE,cAAc,UAAU,EAAE,MAAM,CAAC,CAAC;QACxE,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,IAAI,CAAC;YACH,aAAa,CAAC,aAAa,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;YACrF,UAAU,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;QAClC,CAAC;gBAAS,CAAC;YACT,MAAM,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,CAAC;IACH,CAAC,CAAC;IAEF,OAAO;QACL,YAAY;YACV,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;gBACrB,IAAI,CAAC;oBACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;oBAC/D,IAAI,gBAAgB,CAAC,MAAM,CAAC;wBAAE,OAAO,MAAM,CAAC;gBAC9C,CAAC;gBAAC,MAAM,CAAC;oBACP,oFAAoF;gBACtF,CAAC;YACH,CAAC;YACD,MAAM,KAAK,GAAG,EAAE,eAAe,EAAE,QAAQ,EAAE,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;YACnE,KAAK,CAAC,KAAK,CAAC,CAAC;YACb,OAAO,KAAK,CAAC;QACf,CAAC;QACD,iBAAiB,CAAC,KAAqB;YACrC,MAAM,OAAO,GAAG,EAAE,GAAG,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;YACjD,KAAK,CAAC,OAAO,CAAC,CAAC;YACf,OAAO,OAAO,CAAC;QACjB,CAAC;QACD,MAAM;YACJ,MAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,CAAC;KACF,CAAC;AACJ,CAAC,CAAC"}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Reads the installed Outfitter package version for CLI output and runtime branding.
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
|
+
let cachedVersion;
|
|
3
4
|
export const readOutfitterVersion = () => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
if (cachedVersion === undefined) {
|
|
6
|
+
const packageJsonPath = new URL('../../package.json', import.meta.url);
|
|
7
|
+
cachedVersion = JSON.parse(readFileSync(packageJsonPath, 'utf8')).version;
|
|
8
|
+
}
|
|
9
|
+
return cachedVersion;
|
|
7
10
|
};
|
|
8
11
|
//# sourceMappingURL=OutfitterVersion.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OutfitterVersion.js","sourceRoot":"","sources":["../../src/version/OutfitterVersion.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAW,EAAE;IAC/C,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"OutfitterVersion.js","sourceRoot":"","sources":["../../src/version/OutfitterVersion.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,IAAI,aAAiC,CAAC;AAEtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAW,EAAE;IAC/C,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;QAChC,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACvE,aAAa,GAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAAkC,CAAC,OAAO,CAAC;IAC9G,CAAC;IAED,OAAO,aAAa,CAAC;AACvB,CAAC,CAAC"}
|
|
@@ -47,6 +47,7 @@ One runbook per rung of [the adoption ramp](../philosophy.md#the-ramp-to-an-auto
|
|
|
47
47
|
The same composition runs on every surface; only the trigger changes.
|
|
48
48
|
|
|
49
49
|
- [Running an agent in GitHub Actions](./actions.md) — headless runs on any workflow trigger.
|
|
50
|
+
- [Channels](./channels.md) — add external event sources that wake a resident agent only when work arrives.
|
|
50
51
|
- [Container images](./containers.md) — run the published Debian-based image persistently, extend it with apt, or use the `-nix` variant.
|
|
51
52
|
- [Recurring runs](./recurring-runs.md) — loops three ways: the local loop extension, Actions cron, cluster schedules.
|
|
52
53
|
- [In-cluster agents](./in-cluster.md) — resident agents, CronJobs, and subagent Jobs via Link Operator.
|
|
@@ -71,5 +72,6 @@ Each use case is a worked story — a problem, the composition that answers it,
|
|
|
71
72
|
## Reference
|
|
72
73
|
|
|
73
74
|
- [CLI reference](./cli.md)
|
|
75
|
+
- [Telemetry](./telemetry.md)
|
|
74
76
|
- [Migration from legacy profiles](./migration.md)
|
|
75
77
|
- [Philosophy](../philosophy.md) — why Outfitter exists.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Channels
|
|
2
|
+
|
|
3
|
+
A **channel** is an external source of work an agent watches — a mailbox, Signal,
|
|
4
|
+
GitHub notifications, or a chat service. The
|
|
5
|
+
[`ai-outfitter/channels`](https://github.com/ai-outfitter/channels) Pi extension
|
|
6
|
+
owns those transports as source modules under `extensions/sources/`. A source
|
|
7
|
+
opens its push connection, daemon, or polling loop and sends an **idle-gated
|
|
8
|
+
wake** only when it detects matching work. Multiple sources can run at once and
|
|
9
|
+
feed one notification queue.
|
|
10
|
+
|
|
11
|
+
Every wake is a trusted, body-free signal. Sources that support exact-item
|
|
12
|
+
actions put only an opaque locator in the wake; the agent passes that locator
|
|
13
|
+
unchanged to `channel_read`, which returns the message inside explicit
|
|
14
|
+
untrusted-content markers. It replies through `channel_respond`. Signal-only
|
|
15
|
+
sources instead wake the agent to use that channel's existing client workflow.
|
|
16
|
+
In neither case does an external message enter the session as an instruction.
|
|
17
|
+
|
|
18
|
+
The extension and the response instructions have separate jobs:
|
|
19
|
+
|
|
20
|
+
- `git:github.com/ai-outfitter/channels` supplies event delivery and the common
|
|
21
|
+
channel tools;
|
|
22
|
+
- skills and runbooks in the Channels repository teach the agent how to handle
|
|
23
|
+
each source.
|
|
24
|
+
|
|
25
|
+
Those channel skills are not published by the `community-profiles` catalog. For
|
|
26
|
+
example, Channels currently carries its Slack workflow at
|
|
27
|
+
[`dev/slack-responder/SKILL.md`](https://github.com/ai-outfitter/channels/blob/main/dev/slack-responder/SKILL.md)
|
|
28
|
+
and its operational guides under
|
|
29
|
+
[`docs/runbooks/`](https://github.com/ai-outfitter/channels/tree/main/docs/runbooks).
|
|
30
|
+
|
|
31
|
+
## Add channels to an agent
|
|
32
|
+
|
|
33
|
+
Add the Channels extension source to the agent's [loadout](./agents.md):
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
---
|
|
37
|
+
name: email-assistant
|
|
38
|
+
extensions: [git:github.com/ai-outfitter/channels]
|
|
39
|
+
model: your-model
|
|
40
|
+
---
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Then follow the source-specific skill or runbook in the Channels repository and
|
|
44
|
+
provide that source's credentials. Do not add `mail`, `signal-responder`, or
|
|
45
|
+
`slack-responder` on the assumption that they are community-catalog skill IDs.
|
|
46
|
+
If you want to use the repository's development Slack skill, bring that skill
|
|
47
|
+
into a catalog you control and select its resulting local ID.
|
|
48
|
+
|
|
49
|
+
Because an [agent _is_ the profile](./profiles.md), a multi-channel agent still
|
|
50
|
+
loads the extension only once:
|
|
51
|
+
|
|
52
|
+
```markdown
|
|
53
|
+
---
|
|
54
|
+
name: personal-assistant
|
|
55
|
+
extensions: [git:github.com/ai-outfitter/channels]
|
|
56
|
+
---
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All configured sources feed a single notification queue. Each wake names the
|
|
60
|
+
sources with activity and, when available, includes opaque locators for the
|
|
61
|
+
agent to pass to the common channel tools.
|
|
62
|
+
|
|
63
|
+
## Select which channels run
|
|
64
|
+
|
|
65
|
+
`OUTFITTER_CHANNELS` chooses the active channels; unset means **auto-detect** —
|
|
66
|
+
every channel whose credentials are present starts. So composing a channel into an
|
|
67
|
+
agent is really just supplying its credentials.
|
|
68
|
+
|
|
69
|
+
| `OUTFITTER_CHANNELS` | Behavior |
|
|
70
|
+
| -------------------- | -------------------------- |
|
|
71
|
+
| unset | Auto-detect by credentials |
|
|
72
|
+
| `jmap,signal` | Exactly those channels |
|
|
73
|
+
| `off` / `none` | Disabled |
|
|
74
|
+
|
|
75
|
+
## Credentials per channel
|
|
76
|
+
|
|
77
|
+
Each source reads its configuration from environment variables. Supply them the
|
|
78
|
+
Outfitter way for where the agent runs:
|
|
79
|
+
|
|
80
|
+
- **Local runs** — export them in the shell before `outfitter run` (see the
|
|
81
|
+
[channels README](https://github.com/ai-outfitter/channels) for the bare-pi
|
|
82
|
+
flow).
|
|
83
|
+
- **In-cluster** — project them as env from Secrets via the Kubernetes operator;
|
|
84
|
+
the operator exposes referenced Secrets without inspecting them, and the keys
|
|
85
|
+
inside are each channel's contract.
|
|
86
|
+
|
|
87
|
+
| Source | Delivery | Variables |
|
|
88
|
+
| ------------------------ | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `jmap` (email over JMAP) | JMAP state-change wake | `XIN_BASE_URL`, `XIN_BASIC_USER`, `XIN_BASIC_PASS` |
|
|
90
|
+
| `signal` | `signal-cli` wake | `SIGNAL_NUMBER`, `SIGNAL_CLI_CONFIG` |
|
|
91
|
+
| `slack` | Socket Mode wake plus `channel_read` / `channel_respond` | `SLACK_APP_TOKEN`, `SLACK_BOT_TOKEN`, optional `SLACK_CHANNEL_IDS` |
|
|
92
|
+
| `github` | filtered notification wake | `GITHUB_NOTIFY_TOKEN` (or `GITHUB_TOKEN`), optional `GITHUB_NOTIFY_FILTERS`, `GITHUB_NOTIFY_POLL_MS` |
|
|
93
|
+
|
|
94
|
+
This is only a quick orientation; the Channels README is the source of truth for
|
|
95
|
+
the complete source list, credentials, prerequisites, and behavior. For a
|
|
96
|
+
concrete email deployment, see its
|
|
97
|
+
[Google Workspace agent-mailbox runbook](https://github.com/ai-outfitter/channels/blob/main/docs/runbooks/agent-mailbox-google-workspace.md).
|
|
98
|
+
|
|
99
|
+
## Running resident
|
|
100
|
+
|
|
101
|
+
A channel watcher opens push connections for the life of a session, so it needs a
|
|
102
|
+
long-running agent — an interactive session, `--mode rpc`, or an always-on
|
|
103
|
+
in-cluster deployment. Session switches reopen the connections; one-shot print runs
|
|
104
|
+
are not suitable.
|
|
105
|
+
|
|
106
|
+
## See also
|
|
107
|
+
|
|
108
|
+
- [`ai-outfitter/channels`](https://github.com/ai-outfitter/channels) — the
|
|
109
|
+
extension source, channel source modules, skills, runbooks, and setup.
|
|
110
|
+
- [Skills](./skills.md) — how to bring a repository-owned channel skill into a
|
|
111
|
+
catalog and select it in a loadout.
|
|
112
|
+
- [Agents](./agents.md) / [Profiles](./profiles.md) — loadout and composition.
|
|
@@ -7,6 +7,8 @@ Global options:
|
|
|
7
7
|
| `-V, --version` | Print the Outfitter version. |
|
|
8
8
|
| `-h, --help` | Show help for a command. |
|
|
9
9
|
|
|
10
|
+
See [Telemetry](./telemetry.md) for the pseudonymous analytics event contract and opt-out controls.
|
|
11
|
+
|
|
10
12
|
## `outfitter run [agent] [args...]`
|
|
11
13
|
|
|
12
14
|
Resolve, compose, and launch an agent. `run` is the default command, so plain `outfitter` and `outfitter run` are equivalent.
|
|
@@ -29,6 +31,13 @@ outfitter run engineer --harness codex -- exec "review this repo"
|
|
|
29
31
|
outfitter run persona-reviewer -- --print "summarize this repo"
|
|
30
32
|
```
|
|
31
33
|
|
|
34
|
+
Because `run` is the default command, leading flags that Outfitter does not own are forwarded to the harness automatically. With a configured `default_agent`, the following forms pass flags directly to Pi:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
outfitter -r # equivalent to: outfitter run -- -r
|
|
38
|
+
outfitter --resume # equivalent to: outfitter run -- --resume
|
|
39
|
+
```
|
|
40
|
+
|
|
32
41
|
## `outfitter setup [source]`
|
|
33
42
|
|
|
34
43
|
Open the bundled Pi walkthrough using the original setup wording and sequence. Choose **Use the
|
|
@@ -40,6 +40,10 @@ remote_settings:
|
|
|
40
40
|
ref: 9c47d1e2b8a05f36c4d7e90a12b3f8c5d6e71a04
|
|
41
41
|
|
|
42
42
|
cache_directory: ./cache # optional; relative to this settings file
|
|
43
|
+
|
|
44
|
+
# Pseudonymous product analytics consent; defaults to true when absent.
|
|
45
|
+
telemetry:
|
|
46
|
+
enabled: false
|
|
43
47
|
```
|
|
44
48
|
|
|
45
49
|
- `default_agent` / `default_harness` — which agent plain `outfitter` runs, and the harness it launches in.
|
|
@@ -48,6 +52,7 @@ cache_directory: ./cache # optional; relative to this settings file
|
|
|
48
52
|
- `cache_directory` — the repository cache root used consistently by sync, remote settings, remote
|
|
49
53
|
source resolution, and default-catalog setup. It defaults to `~/.agents/cache`; repositories live
|
|
50
54
|
below its `repos/` directory.
|
|
55
|
+
- `telemetry.enabled` — the primary and sole persistent control for pseudonymous product analytics. Edit it directly to enable or disable telemetry. See [Telemetry](./telemetry.md) for consent precedence, automatic identifier cleanup, the event contract, and the current inert-build status.
|
|
51
56
|
|
|
52
57
|
## Precedence
|
|
53
58
|
|