@orgloop/cli 0.1.0
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/LICENSE.md +21 -0
- package/dist/commands/add.d.ts +8 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +363 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/apply.d.ts +9 -0
- package/dist/commands/apply.d.ts.map +1 -0
- package/dist/commands/apply.js +289 -0
- package/dist/commands/apply.js.map +1 -0
- package/dist/commands/doctor.d.ts +45 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +346 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/env.d.ts +23 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +230 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/hook.d.ts +16 -0
- package/dist/commands/hook.d.ts.map +1 -0
- package/dist/commands/hook.js +80 -0
- package/dist/commands/hook.js.map +1 -0
- package/dist/commands/init.d.ts +42 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +532 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/inspect.d.ts +8 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +177 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/install-service.d.ts +8 -0
- package/dist/commands/install-service.d.ts.map +1 -0
- package/dist/commands/install-service.js +182 -0
- package/dist/commands/install-service.js.map +1 -0
- package/dist/commands/logs.d.ts +8 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +197 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/plan.d.ts +9 -0
- package/dist/commands/plan.d.ts.map +1 -0
- package/dist/commands/plan.js +200 -0
- package/dist/commands/plan.js.map +1 -0
- package/dist/commands/routes.d.ts +37 -0
- package/dist/commands/routes.d.ts.map +1 -0
- package/dist/commands/routes.js +167 -0
- package/dist/commands/routes.js.map +1 -0
- package/dist/commands/service.d.ts +9 -0
- package/dist/commands/service.d.ts.map +1 -0
- package/dist/commands/service.js +178 -0
- package/dist/commands/service.js.map +1 -0
- package/dist/commands/status.d.ts +8 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +176 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/stop.d.ts +8 -0
- package/dist/commands/stop.d.ts.map +1 -0
- package/dist/commands/stop.js +102 -0
- package/dist/commands/stop.js.map +1 -0
- package/dist/commands/test.d.ts +8 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +236 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/validate.d.ts +33 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +501 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/version.d.ts +8 -0
- package/dist/commands/version.d.ts.map +1 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +169 -0
- package/dist/config.js.map +1 -0
- package/dist/env-metadata.d.ts +12 -0
- package/dist/env-metadata.d.ts.map +1 -0
- package/dist/env-metadata.js +39 -0
- package/dist/env-metadata.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/module-resolver.d.ts +42 -0
- package/dist/module-resolver.d.ts.map +1 -0
- package/dist/module-resolver.js +134 -0
- package/dist/module-resolver.js.map +1 -0
- package/dist/output.d.ts +36 -0
- package/dist/output.d.ts.map +1 -0
- package/dist/output.js +142 -0
- package/dist/output.js.map +1 -0
- package/dist/resolve-connectors.d.ts +27 -0
- package/dist/resolve-connectors.d.ts.map +1 -0
- package/dist/resolve-connectors.js +94 -0
- package/dist/resolve-connectors.js.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orgloop hook — Forward hook events to a running OrgLoop engine.
|
|
3
|
+
*
|
|
4
|
+
* Reads stdin and POSTs the raw JSON body to the engine's webhook endpoint.
|
|
5
|
+
* This is a stdin-to-HTTP bridge — the connector's webhook handler builds
|
|
6
|
+
* the OrgLoopEvent from the raw payload.
|
|
7
|
+
*/
|
|
8
|
+
// ─── Constants ──────────────────────────────────────────────────────────────
|
|
9
|
+
const DEFAULT_PORT = 4800;
|
|
10
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
11
|
+
function resolvePort(portFlag) {
|
|
12
|
+
if (portFlag) {
|
|
13
|
+
const n = Number.parseInt(portFlag, 10);
|
|
14
|
+
if (!Number.isNaN(n) && n > 0 && n <= 65535)
|
|
15
|
+
return n;
|
|
16
|
+
}
|
|
17
|
+
const envPort = process.env.ORGLOOP_PORT;
|
|
18
|
+
if (envPort) {
|
|
19
|
+
const n = Number.parseInt(envPort, 10);
|
|
20
|
+
if (!Number.isNaN(n) && n > 0 && n <= 65535)
|
|
21
|
+
return n;
|
|
22
|
+
}
|
|
23
|
+
return DEFAULT_PORT;
|
|
24
|
+
}
|
|
25
|
+
export function readStdin() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
let data = '';
|
|
28
|
+
process.stdin.setEncoding('utf-8');
|
|
29
|
+
process.stdin.on('data', (chunk) => {
|
|
30
|
+
data += chunk;
|
|
31
|
+
});
|
|
32
|
+
process.stdin.on('end', () => resolve(data));
|
|
33
|
+
process.stdin.on('error', reject);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export async function postToWebhook(sourceId, body, port) {
|
|
37
|
+
const url = `http://127.0.0.1:${port}/webhook/${sourceId}`;
|
|
38
|
+
const res = await fetch(url, {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: { 'Content-Type': 'application/json' },
|
|
41
|
+
body,
|
|
42
|
+
});
|
|
43
|
+
const text = await res.text();
|
|
44
|
+
return { ok: res.ok, status: res.status, body: text };
|
|
45
|
+
}
|
|
46
|
+
// ─── Command registration ────────────────────────────────────────────────────
|
|
47
|
+
export function registerHookCommand(program) {
|
|
48
|
+
const hook = program.command('hook').description('Forward hook events to running OrgLoop engine');
|
|
49
|
+
hook
|
|
50
|
+
.command('claude-code-stop')
|
|
51
|
+
.description('Forward Claude Code stop hook event')
|
|
52
|
+
.option('--port <port>', 'Engine webhook port')
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
const port = resolvePort(opts.port);
|
|
55
|
+
let body;
|
|
56
|
+
try {
|
|
57
|
+
body = await readStdin();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
process.stderr.write('orgloop hook: failed to read stdin\n');
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const result = await postToWebhook('claude-code', body, port);
|
|
66
|
+
if (!result.ok) {
|
|
67
|
+
process.stderr.write(`orgloop hook: webhook returned ${result.status}: ${result.body}\n`);
|
|
68
|
+
process.exitCode = 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const msg = err instanceof Error && 'code' in err && err.code === 'ECONNREFUSED'
|
|
73
|
+
? 'OrgLoop engine is not running. Start it with: orgloop apply'
|
|
74
|
+
: `Failed to deliver hook event: ${err instanceof Error ? err.message : String(err)}`;
|
|
75
|
+
process.stderr.write(`orgloop hook: ${msg}\n`);
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../../src/commands/hook.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orgloop init — Scaffold a new OrgLoop project.
|
|
3
|
+
*
|
|
4
|
+
* Interactive mode (default): prompts for project name, description, connectors.
|
|
5
|
+
* Non-interactive: --name, --connectors, --no-interactive flags.
|
|
6
|
+
*/
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
/**
|
|
9
|
+
* Scan connector YAML for ${VAR} references and return a map of
|
|
10
|
+
* var name → connector file that requires it.
|
|
11
|
+
*/
|
|
12
|
+
export declare function collectEnvVars(connectors: string[]): Map<string, string>;
|
|
13
|
+
/**
|
|
14
|
+
* Build the contents for a .env.example file from collected env vars.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildEnvExampleContent(envVars: Map<string, string>): string;
|
|
17
|
+
/**
|
|
18
|
+
* Build a Claude Code Stop hook entry in the object format expected by
|
|
19
|
+
* Claude Code's settings.json.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildClaudeCodeHookEntry(command: string): {
|
|
22
|
+
matcher: string;
|
|
23
|
+
hooks: {
|
|
24
|
+
type: string;
|
|
25
|
+
command: string;
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Check whether a Stop hooks array already contains an orgloop hook.
|
|
30
|
+
* Handles the object format: [{ matcher, hooks: [{ type, command }] }].
|
|
31
|
+
*/
|
|
32
|
+
export declare function hasExistingOrgloopHook(stopHooks: unknown[]): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Merge an orgloop hook into a settings object. Returns the updated settings
|
|
35
|
+
* and a boolean indicating whether the hook was already present.
|
|
36
|
+
*/
|
|
37
|
+
export declare function mergeClaudeCodeHook(settings: Record<string, unknown>, hookCommand: string): {
|
|
38
|
+
settings: Record<string, unknown>;
|
|
39
|
+
alreadyInstalled: boolean;
|
|
40
|
+
};
|
|
41
|
+
export declare function registerInitCommand(program: Command): void;
|
|
42
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyOzC;;;GAGG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxE;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAgB3E;AAiGD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM;;;;;;EAKvD;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,OAAO,CAOpE;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,WAAW,EAAE,MAAM,GACjB;IAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,CAYlE;AA0DD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAyH1D"}
|
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orgloop init — Scaffold a new OrgLoop project.
|
|
3
|
+
*
|
|
4
|
+
* Interactive mode (default): prompts for project name, description, connectors.
|
|
5
|
+
* Non-interactive: --name, --connectors, --no-interactive flags.
|
|
6
|
+
*/
|
|
7
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { join, resolve } from 'node:path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { getEnvVarMeta } from '../env-metadata.js';
|
|
12
|
+
import * as output from '../output.js';
|
|
13
|
+
// ─── Connectors ──────────────────────────────────────────────────────────────
|
|
14
|
+
const AVAILABLE_CONNECTORS = [
|
|
15
|
+
'github',
|
|
16
|
+
'linear',
|
|
17
|
+
'openclaw',
|
|
18
|
+
'claude-code',
|
|
19
|
+
'webhook',
|
|
20
|
+
'slack',
|
|
21
|
+
'pagerduty',
|
|
22
|
+
];
|
|
23
|
+
function connectorYaml(name, role) {
|
|
24
|
+
const configs = {
|
|
25
|
+
github: `apiVersion: orgloop/v1alpha1
|
|
26
|
+
kind: ConnectorGroup
|
|
27
|
+
|
|
28
|
+
sources:
|
|
29
|
+
- id: github
|
|
30
|
+
description: GitHub repository events
|
|
31
|
+
connector: "@orgloop/connector-github"
|
|
32
|
+
config:
|
|
33
|
+
repo: "\${GITHUB_REPO}"
|
|
34
|
+
token: "\${GITHUB_TOKEN}"
|
|
35
|
+
events:
|
|
36
|
+
- "pull_request.review_submitted"
|
|
37
|
+
- "pull_request_review_comment"
|
|
38
|
+
- "issue_comment"
|
|
39
|
+
- "pull_request.closed"
|
|
40
|
+
- "pull_request.merged"
|
|
41
|
+
- "workflow_run.completed"
|
|
42
|
+
poll:
|
|
43
|
+
interval: "5m"
|
|
44
|
+
emits:
|
|
45
|
+
- resource.changed
|
|
46
|
+
`,
|
|
47
|
+
linear: `apiVersion: orgloop/v1alpha1
|
|
48
|
+
kind: ConnectorGroup
|
|
49
|
+
|
|
50
|
+
sources:
|
|
51
|
+
- id: linear
|
|
52
|
+
description: Linear project tracking events
|
|
53
|
+
connector: "@orgloop/connector-linear"
|
|
54
|
+
config:
|
|
55
|
+
team: "\${LINEAR_TEAM_KEY}"
|
|
56
|
+
api_key: "\${LINEAR_API_KEY}"
|
|
57
|
+
poll:
|
|
58
|
+
interval: "5m"
|
|
59
|
+
emits:
|
|
60
|
+
- resource.changed`,
|
|
61
|
+
openclaw: `apiVersion: orgloop/v1alpha1
|
|
62
|
+
kind: ConnectorGroup
|
|
63
|
+
|
|
64
|
+
actors:
|
|
65
|
+
- id: openclaw-engineering-agent
|
|
66
|
+
description: OpenClaw engineering agent
|
|
67
|
+
connector: "@orgloop/connector-openclaw"
|
|
68
|
+
config:
|
|
69
|
+
base_url: "http://127.0.0.1:18789"
|
|
70
|
+
auth_token_env: "\${OPENCLAW_WEBHOOK_TOKEN}"
|
|
71
|
+
agent_id: "\${OPENCLAW_AGENT_ID}"`,
|
|
72
|
+
'claude-code': `apiVersion: orgloop/v1alpha1
|
|
73
|
+
kind: ConnectorGroup
|
|
74
|
+
|
|
75
|
+
sources:
|
|
76
|
+
- id: claude-code
|
|
77
|
+
description: Claude Code session events
|
|
78
|
+
connector: "@orgloop/connector-claude-code"
|
|
79
|
+
config:
|
|
80
|
+
hook_type: post-exit
|
|
81
|
+
emits:
|
|
82
|
+
- actor.stopped`,
|
|
83
|
+
webhook: `apiVersion: orgloop/v1alpha1
|
|
84
|
+
kind: ConnectorGroup
|
|
85
|
+
|
|
86
|
+
sources:
|
|
87
|
+
- id: webhook
|
|
88
|
+
description: Generic webhook receiver
|
|
89
|
+
connector: "@orgloop/connector-webhook"
|
|
90
|
+
config:
|
|
91
|
+
path: "/webhook"
|
|
92
|
+
emits:
|
|
93
|
+
- resource.changed
|
|
94
|
+
- message.received`,
|
|
95
|
+
slack: `apiVersion: orgloop/v1alpha1
|
|
96
|
+
kind: ConnectorGroup
|
|
97
|
+
|
|
98
|
+
actors:
|
|
99
|
+
- id: slack-notify
|
|
100
|
+
description: Slack notification delivery
|
|
101
|
+
connector: "@orgloop/connector-webhook"
|
|
102
|
+
config:
|
|
103
|
+
url: "\${SLACK_WEBHOOK_URL}"`,
|
|
104
|
+
pagerduty: `apiVersion: orgloop/v1alpha1
|
|
105
|
+
kind: ConnectorGroup
|
|
106
|
+
|
|
107
|
+
actors:
|
|
108
|
+
- id: pagerduty
|
|
109
|
+
description: PagerDuty incident delivery
|
|
110
|
+
connector: "@orgloop/connector-webhook"
|
|
111
|
+
config:
|
|
112
|
+
url: "\${PAGERDUTY_WEBHOOK_URL}"`,
|
|
113
|
+
};
|
|
114
|
+
return configs[name] ?? configs.webhook;
|
|
115
|
+
}
|
|
116
|
+
function generateOrgloopYaml(name, description, connectors) {
|
|
117
|
+
const connectorRefs = connectors.map((c) => ` - connectors/${c}.yaml`).join('\n');
|
|
118
|
+
return `apiVersion: orgloop/v1alpha1
|
|
119
|
+
kind: Project
|
|
120
|
+
|
|
121
|
+
metadata:
|
|
122
|
+
name: ${name}
|
|
123
|
+
description: "${description}"
|
|
124
|
+
|
|
125
|
+
defaults:
|
|
126
|
+
poll_interval: "5m"
|
|
127
|
+
event_retention: "30d"
|
|
128
|
+
log_level: info
|
|
129
|
+
|
|
130
|
+
connectors:
|
|
131
|
+
${connectorRefs}
|
|
132
|
+
|
|
133
|
+
transforms:
|
|
134
|
+
- transforms/transforms.yaml
|
|
135
|
+
|
|
136
|
+
loggers:
|
|
137
|
+
- loggers/default.yaml
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
function generateDefaultRouteYaml() {
|
|
141
|
+
return `apiVersion: orgloop/v1alpha1
|
|
142
|
+
kind: RouteGroup
|
|
143
|
+
|
|
144
|
+
routes:
|
|
145
|
+
- name: example-route
|
|
146
|
+
description: Example route — customize for your setup
|
|
147
|
+
when:
|
|
148
|
+
source: github
|
|
149
|
+
events:
|
|
150
|
+
- resource.changed
|
|
151
|
+
transforms:
|
|
152
|
+
- ref: drop-bot-noise
|
|
153
|
+
then:
|
|
154
|
+
actor: openclaw-engineering-agent
|
|
155
|
+
with:
|
|
156
|
+
prompt_file: ../sops/example.md
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
function generateDefaultTransformsYaml() {
|
|
160
|
+
return `apiVersion: orgloop/v1alpha1
|
|
161
|
+
kind: TransformGroup
|
|
162
|
+
|
|
163
|
+
transforms:
|
|
164
|
+
- name: drop-bot-noise
|
|
165
|
+
type: script
|
|
166
|
+
script: ./drop-bot-noise.sh
|
|
167
|
+
timeout_ms: 5000
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
function generateDropBotScript() {
|
|
171
|
+
return `#!/usr/bin/env bash
|
|
172
|
+
# drop-bot-noise.sh — Drop events from known bot authors.
|
|
173
|
+
#
|
|
174
|
+
# Reads OrgLoop event JSON from stdin.
|
|
175
|
+
# Exit 0 = PASS (forward event), Exit 78 = DROP (discard event).
|
|
176
|
+
|
|
177
|
+
set -euo pipefail
|
|
178
|
+
|
|
179
|
+
EVENT=$(cat)
|
|
180
|
+
AUTHOR_TYPE=$(echo "$EVENT" | jq -r '.provenance.author_type // "unknown"')
|
|
181
|
+
|
|
182
|
+
if [ "$AUTHOR_TYPE" = "bot" ]; then
|
|
183
|
+
exit 78 # DROP
|
|
184
|
+
fi
|
|
185
|
+
|
|
186
|
+
# PASS — forward the event unchanged
|
|
187
|
+
echo "$EVENT"
|
|
188
|
+
exit 0
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
function generateDefaultLoggerYaml() {
|
|
192
|
+
return `apiVersion: orgloop/v1alpha1
|
|
193
|
+
kind: LoggerGroup
|
|
194
|
+
|
|
195
|
+
loggers:
|
|
196
|
+
- name: file-log
|
|
197
|
+
type: "@orgloop/logger-file"
|
|
198
|
+
config:
|
|
199
|
+
path: "~/.orgloop/logs/orgloop.log"
|
|
200
|
+
format: jsonl
|
|
201
|
+
rotate:
|
|
202
|
+
max_size: "50MB"
|
|
203
|
+
max_files: 10
|
|
204
|
+
`;
|
|
205
|
+
}
|
|
206
|
+
function generateExampleSop() {
|
|
207
|
+
return `# Example Launch Prompt
|
|
208
|
+
|
|
209
|
+
You are receiving an event from the organization pipeline.
|
|
210
|
+
|
|
211
|
+
## Context
|
|
212
|
+
This event was routed through OrgLoop based on the configured rules.
|
|
213
|
+
|
|
214
|
+
## Instructions
|
|
215
|
+
1. Review the event payload
|
|
216
|
+
2. Take appropriate action based on the event type
|
|
217
|
+
3. Report completion status
|
|
218
|
+
|
|
219
|
+
## Constraints
|
|
220
|
+
- Do not modify production infrastructure without approval
|
|
221
|
+
- Follow the organization's coding standards
|
|
222
|
+
- Escalate security-related events immediately
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
// ─── Env var collection ──────────────────────────────────────────────────────
|
|
226
|
+
/**
|
|
227
|
+
* Scan connector YAML for ${VAR} references and return a map of
|
|
228
|
+
* var name → connector file that requires it.
|
|
229
|
+
*/
|
|
230
|
+
export function collectEnvVars(connectors) {
|
|
231
|
+
const envVars = new Map();
|
|
232
|
+
for (const conn of connectors) {
|
|
233
|
+
const yamlContent = connectorYaml(conn, ['openclaw', 'slack', 'pagerduty'].includes(conn) ? 'actor' : 'source');
|
|
234
|
+
const matches = yamlContent.matchAll(/\$\{([^}]+)\}/g);
|
|
235
|
+
for (const match of matches) {
|
|
236
|
+
envVars.set(match[1], `connectors/${conn}.yaml`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return envVars;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Build the contents for a .env.example file from collected env vars.
|
|
243
|
+
*/
|
|
244
|
+
export function buildEnvExampleContent(envVars) {
|
|
245
|
+
const lines = [
|
|
246
|
+
'# OrgLoop environment variables',
|
|
247
|
+
'# Copy to .env and fill in values',
|
|
248
|
+
'',
|
|
249
|
+
];
|
|
250
|
+
for (const [varName] of envVars) {
|
|
251
|
+
const meta = getEnvVarMeta(varName);
|
|
252
|
+
if (meta) {
|
|
253
|
+
lines.push(`# ${meta.description}`);
|
|
254
|
+
if (meta.help_url)
|
|
255
|
+
lines.push(`# ${meta.help_url}`);
|
|
256
|
+
}
|
|
257
|
+
lines.push(`# ${varName}=`);
|
|
258
|
+
lines.push('');
|
|
259
|
+
}
|
|
260
|
+
return lines.join('\n');
|
|
261
|
+
}
|
|
262
|
+
// ─── File creation ───────────────────────────────────────────────────────────
|
|
263
|
+
async function dirExists(path) {
|
|
264
|
+
try {
|
|
265
|
+
await access(path);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function scaffoldProject(targetDir, name, description, connectors) {
|
|
273
|
+
const created = [];
|
|
274
|
+
// Create directories
|
|
275
|
+
await mkdir(join(targetDir, 'connectors'), { recursive: true });
|
|
276
|
+
await mkdir(join(targetDir, 'routes'), { recursive: true });
|
|
277
|
+
await mkdir(join(targetDir, 'transforms'), { recursive: true });
|
|
278
|
+
await mkdir(join(targetDir, 'loggers'), { recursive: true });
|
|
279
|
+
await mkdir(join(targetDir, 'sops'), { recursive: true });
|
|
280
|
+
// orgloop.yaml
|
|
281
|
+
const orgloopPath = join(targetDir, 'orgloop.yaml');
|
|
282
|
+
await writeFile(orgloopPath, generateOrgloopYaml(name, description, connectors), 'utf-8');
|
|
283
|
+
created.push('orgloop.yaml');
|
|
284
|
+
// Connector files
|
|
285
|
+
for (const conn of connectors) {
|
|
286
|
+
const connPath = join(targetDir, 'connectors', `${conn}.yaml`);
|
|
287
|
+
const role = ['openclaw', 'slack', 'pagerduty'].includes(conn) ? 'actor' : 'source';
|
|
288
|
+
await writeFile(connPath, connectorYaml(conn, role), 'utf-8');
|
|
289
|
+
created.push(`connectors/${conn}.yaml`);
|
|
290
|
+
}
|
|
291
|
+
// Route files
|
|
292
|
+
const routePath = join(targetDir, 'routes', 'example.yaml');
|
|
293
|
+
await writeFile(routePath, generateDefaultRouteYaml(), 'utf-8');
|
|
294
|
+
created.push('routes/example.yaml');
|
|
295
|
+
// Logger files
|
|
296
|
+
const loggerPath = join(targetDir, 'loggers', 'default.yaml');
|
|
297
|
+
await writeFile(loggerPath, generateDefaultLoggerYaml(), 'utf-8');
|
|
298
|
+
created.push('loggers/default.yaml');
|
|
299
|
+
// Transform files
|
|
300
|
+
const transformsYamlPath = join(targetDir, 'transforms', 'transforms.yaml');
|
|
301
|
+
await writeFile(transformsYamlPath, generateDefaultTransformsYaml(), 'utf-8');
|
|
302
|
+
created.push('transforms/transforms.yaml');
|
|
303
|
+
const scriptPath = join(targetDir, 'transforms', 'drop-bot-noise.sh');
|
|
304
|
+
await writeFile(scriptPath, generateDropBotScript(), 'utf-8');
|
|
305
|
+
const { chmod } = await import('node:fs/promises');
|
|
306
|
+
await chmod(scriptPath, 0o755);
|
|
307
|
+
created.push('transforms/drop-bot-noise.sh');
|
|
308
|
+
// SOP files
|
|
309
|
+
const sopPath = join(targetDir, 'sops', 'example.md');
|
|
310
|
+
await writeFile(sopPath, generateExampleSop(), 'utf-8');
|
|
311
|
+
created.push('sops/example.md');
|
|
312
|
+
// Generate .env.example
|
|
313
|
+
const envVars = collectEnvVars(connectors);
|
|
314
|
+
if (envVars.size > 0) {
|
|
315
|
+
await writeFile(join(targetDir, '.env.example'), buildEnvExampleContent(envVars), 'utf-8');
|
|
316
|
+
created.push('.env.example');
|
|
317
|
+
}
|
|
318
|
+
// Generate .gitignore (only if it doesn't already exist)
|
|
319
|
+
const gitignorePath = join(targetDir, '.gitignore');
|
|
320
|
+
if (!(await dirExists(gitignorePath))) {
|
|
321
|
+
const gitignoreContent = `# Environment variables (contains secrets)
|
|
322
|
+
.env
|
|
323
|
+
.env.local
|
|
324
|
+
|
|
325
|
+
# OrgLoop runtime
|
|
326
|
+
.orgloop/
|
|
327
|
+
|
|
328
|
+
# Node
|
|
329
|
+
node_modules/
|
|
330
|
+
dist/
|
|
331
|
+
`;
|
|
332
|
+
await writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
333
|
+
created.push('.gitignore');
|
|
334
|
+
}
|
|
335
|
+
return created;
|
|
336
|
+
}
|
|
337
|
+
// ─── Claude Code hook helpers (exported for testing) ─────────────────────────
|
|
338
|
+
/**
|
|
339
|
+
* Build a Claude Code Stop hook entry in the object format expected by
|
|
340
|
+
* Claude Code's settings.json.
|
|
341
|
+
*/
|
|
342
|
+
export function buildClaudeCodeHookEntry(command) {
|
|
343
|
+
return {
|
|
344
|
+
matcher: '',
|
|
345
|
+
hooks: [{ type: 'command', command }],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Check whether a Stop hooks array already contains an orgloop hook.
|
|
350
|
+
* Handles the object format: [{ matcher, hooks: [{ type, command }] }].
|
|
351
|
+
*/
|
|
352
|
+
export function hasExistingOrgloopHook(stopHooks) {
|
|
353
|
+
return stopHooks.some((entry) => {
|
|
354
|
+
if (typeof entry !== 'object' || entry === null)
|
|
355
|
+
return false;
|
|
356
|
+
const obj = entry;
|
|
357
|
+
const innerHooks = obj.hooks;
|
|
358
|
+
return innerHooks?.some((h) => typeof h.command === 'string' && h.command.includes('orgloop'));
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Merge an orgloop hook into a settings object. Returns the updated settings
|
|
363
|
+
* and a boolean indicating whether the hook was already present.
|
|
364
|
+
*/
|
|
365
|
+
export function mergeClaudeCodeHook(settings, hookCommand) {
|
|
366
|
+
const hooks = (settings.hooks ?? {});
|
|
367
|
+
const stopHooks = (hooks.Stop ?? []);
|
|
368
|
+
if (hasExistingOrgloopHook(stopHooks)) {
|
|
369
|
+
return { settings, alreadyInstalled: true };
|
|
370
|
+
}
|
|
371
|
+
stopHooks.push(buildClaudeCodeHookEntry(hookCommand));
|
|
372
|
+
hooks.Stop = stopHooks;
|
|
373
|
+
settings.hooks = hooks;
|
|
374
|
+
return { settings, alreadyInstalled: false };
|
|
375
|
+
}
|
|
376
|
+
// ─── Claude Code hook onboarding ──────────────────────────────────────────────
|
|
377
|
+
async function promptClaudeCodeHook() {
|
|
378
|
+
const { default: inquirer } = await import('inquirer');
|
|
379
|
+
const { scope } = await inquirer.prompt([
|
|
380
|
+
{
|
|
381
|
+
type: 'list',
|
|
382
|
+
name: 'scope',
|
|
383
|
+
message: 'Install OrgLoop hook to Claude Code settings?',
|
|
384
|
+
choices: [
|
|
385
|
+
{ name: 'Global (~/.claude/settings.json)', value: 'global' },
|
|
386
|
+
{ name: 'Project (.claude/settings.json)', value: 'project' },
|
|
387
|
+
{ name: 'Skip', value: 'skip' },
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
]);
|
|
391
|
+
if (scope === 'skip')
|
|
392
|
+
return;
|
|
393
|
+
const settingsPath = scope === 'global'
|
|
394
|
+
? join(homedir(), '.claude', 'settings.json')
|
|
395
|
+
: join(process.cwd(), '.claude', 'settings.json');
|
|
396
|
+
const hookCommand = 'orgloop hook claude-code-stop';
|
|
397
|
+
try {
|
|
398
|
+
let settings = {};
|
|
399
|
+
try {
|
|
400
|
+
const content = await readFile(settingsPath, 'utf-8');
|
|
401
|
+
settings = JSON.parse(content);
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
// File doesn't exist yet
|
|
405
|
+
}
|
|
406
|
+
const result = mergeClaudeCodeHook(settings, hookCommand);
|
|
407
|
+
if (result.alreadyInstalled) {
|
|
408
|
+
output.info(' OrgLoop hook already installed in Claude Code settings.');
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
await mkdir(join(settingsPath, '..'), { recursive: true });
|
|
412
|
+
await writeFile(settingsPath, `${JSON.stringify(result.settings, null, 2)}\n`, 'utf-8');
|
|
413
|
+
output.success(` Installed Claude Code Stop hook → ${chalk.dim(settingsPath)}`);
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
output.warn(` Could not install hook: ${err instanceof Error ? err.message : String(err)}`);
|
|
417
|
+
output.info(` Manually add to ${settingsPath}:`);
|
|
418
|
+
output.info(` "hooks": { "Stop": [{ "matcher": "", "hooks": [{ "type": "command", "command": "${hookCommand}" }] }] }`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
// ─── Command registration ────────────────────────────────────────────────────
|
|
422
|
+
export function registerInitCommand(program) {
|
|
423
|
+
program
|
|
424
|
+
.command('init')
|
|
425
|
+
.description('Scaffold a new OrgLoop project')
|
|
426
|
+
.option('--name <name>', 'Project name')
|
|
427
|
+
.option('--description <desc>', 'Project description')
|
|
428
|
+
.option('--connectors <list>', 'Comma-separated connector list')
|
|
429
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
430
|
+
.option('--dir <path>', 'Target directory (default: current directory)')
|
|
431
|
+
.action(async (opts) => {
|
|
432
|
+
try {
|
|
433
|
+
const targetDir = opts.dir ? resolve(opts.dir) : process.cwd();
|
|
434
|
+
let name;
|
|
435
|
+
let description;
|
|
436
|
+
let connectors;
|
|
437
|
+
if (opts.interactive === false) {
|
|
438
|
+
// Non-interactive mode
|
|
439
|
+
name = opts.name ?? 'my-org';
|
|
440
|
+
description = opts.description ?? 'OrgLoop project';
|
|
441
|
+
connectors = opts.connectors
|
|
442
|
+
? opts.connectors.split(',').map((c) => c.trim())
|
|
443
|
+
: ['github'];
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
// Interactive mode
|
|
447
|
+
const { default: inquirer } = await import('inquirer');
|
|
448
|
+
const answers = await inquirer.prompt([
|
|
449
|
+
{
|
|
450
|
+
type: 'input',
|
|
451
|
+
name: 'name',
|
|
452
|
+
message: 'Project name:',
|
|
453
|
+
default: opts.name ?? 'my-org',
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
type: 'input',
|
|
457
|
+
name: 'description',
|
|
458
|
+
message: 'Description:',
|
|
459
|
+
default: opts.description ?? 'Organization event routing',
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
type: 'checkbox',
|
|
463
|
+
name: 'connectors',
|
|
464
|
+
message: 'Which connectors?',
|
|
465
|
+
choices: AVAILABLE_CONNECTORS.map((c) => ({
|
|
466
|
+
name: c.charAt(0).toUpperCase() + c.slice(1),
|
|
467
|
+
value: c,
|
|
468
|
+
checked: ['github', 'linear', 'openclaw', 'claude-code'].includes(c),
|
|
469
|
+
})),
|
|
470
|
+
},
|
|
471
|
+
]);
|
|
472
|
+
name = answers.name;
|
|
473
|
+
description = answers.description;
|
|
474
|
+
connectors = answers.connectors;
|
|
475
|
+
}
|
|
476
|
+
// Validate connectors
|
|
477
|
+
for (const c of connectors) {
|
|
478
|
+
if (!AVAILABLE_CONNECTORS.includes(c)) {
|
|
479
|
+
output.error(`Unknown connector: ${c}`);
|
|
480
|
+
output.info(`Available: ${AVAILABLE_CONNECTORS.join(', ')}`);
|
|
481
|
+
process.exitCode = 1;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// Check for existing orgloop.yaml
|
|
486
|
+
if (await dirExists(join(targetDir, 'orgloop.yaml'))) {
|
|
487
|
+
output.error('orgloop.yaml already exists in this directory.');
|
|
488
|
+
output.info('Use a different directory or remove the existing file.');
|
|
489
|
+
process.exitCode = 1;
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const created = await scaffoldProject(targetDir, name, description, connectors);
|
|
493
|
+
output.blank();
|
|
494
|
+
output.heading('Created:');
|
|
495
|
+
for (const file of created) {
|
|
496
|
+
output.info(` ${file}`);
|
|
497
|
+
}
|
|
498
|
+
// Show env var status with ✓/✗ indicators
|
|
499
|
+
const envVars = collectEnvVars(connectors);
|
|
500
|
+
if (envVars.size > 0) {
|
|
501
|
+
output.blank();
|
|
502
|
+
output.heading('Environment variables:');
|
|
503
|
+
for (const [varName, file] of envVars) {
|
|
504
|
+
const isSet = process.env[varName] !== undefined;
|
|
505
|
+
const icon = isSet ? chalk.green('✓') : chalk.red('✗');
|
|
506
|
+
output.info(` ${icon} ${chalk.yellow(varName.padEnd(22))} ${chalk.dim(file)}`);
|
|
507
|
+
if (!isSet) {
|
|
508
|
+
const meta = getEnvVarMeta(varName);
|
|
509
|
+
if (meta) {
|
|
510
|
+
output.info(` ${chalk.dim('\u2192')} ${meta.description}`);
|
|
511
|
+
if (meta.help_url) {
|
|
512
|
+
output.info(` ${chalk.dim('\u2192')} ${chalk.cyan(meta.help_url)}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
// Claude Code hook onboarding
|
|
519
|
+
if (connectors.includes('claude-code') && opts.interactive !== false) {
|
|
520
|
+
output.blank();
|
|
521
|
+
await promptClaudeCodeHook();
|
|
522
|
+
}
|
|
523
|
+
output.blank();
|
|
524
|
+
output.info(chalk.dim('Next: run `orgloop add module <name>` to install a workflow module, or `orgloop doctor` to check your environment.'));
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
output.error(`Init failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
528
|
+
process.exitCode = 1;
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
//# sourceMappingURL=init.js.map
|