@aion0/forge 0.10.79 → 0.10.81

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.
@@ -80,6 +80,7 @@ export function installForgeSkills(
80
80
  function ensureForgePermissions(projectPath: string): void {
81
81
  const settingsFile = join(projectPath, '.claude', 'settings.json');
82
82
  const FORGE_CURL_ALLOW = 'Bash(curl*localhost*/smith*)';
83
+ const FORGE_TASK_CURL_ALLOW = 'Bash(curl*localhost*/api/tasks*)';
83
84
 
84
85
  try {
85
86
  let settings: any = {};
@@ -102,14 +103,21 @@ function ensureForgePermissions(projectPath: string): void {
102
103
  });
103
104
  if (settings.permissions.deny.length !== denyBefore) changed = true;
104
105
 
105
- // Add forge curl allow if not present
106
- const hasForgeAllow = settings.permissions.allow.some((rule: string) =>
106
+ // Add forge curl allows if not present
107
+ const hasSmithAllow = settings.permissions.allow.some((rule: string) =>
107
108
  rule.includes('localhost') && rule.includes('smith')
108
109
  );
109
- if (!hasForgeAllow) {
110
+ if (!hasSmithAllow) {
110
111
  settings.permissions.allow.push(FORGE_CURL_ALLOW);
111
112
  changed = true;
112
113
  }
114
+ const hasTaskAllow = settings.permissions.allow.some((rule: string) =>
115
+ rule.includes('localhost') && rule.includes('/api/tasks')
116
+ );
117
+ if (!hasTaskAllow) {
118
+ settings.permissions.allow.push(FORGE_TASK_CURL_ALLOW);
119
+ changed = true;
120
+ }
113
121
 
114
122
  if (changed) {
115
123
  mkdirSync(join(projectPath, '.claude'), { recursive: true });
@@ -175,6 +183,7 @@ const FORGE_HOOK_MARKER = '# forge-stop-hook';
175
183
  /**
176
184
  * Install a Stop hook in user-level ~/.claude/settings.json.
177
185
  * When Claude Code finishes a turn, the hook notifies Forge via HTTP.
186
+ * Handles both workspace agent completion and tmux task completion.
178
187
  * Preserves existing user hooks. Creates backup before modifying.
179
188
  */
180
189
  // Auto-prune leaves this many timestamped backups behind. `forge-backup-manual`
@@ -182,17 +191,18 @@ const FORGE_HOOK_MARKER = '# forge-stop-hook';
182
191
  const FORGE_BACKUP_KEEP = 5;
183
192
  const FORGE_BACKUP_RE = /^settings\.json\.forge-backup-\d{8}-\d{4}$/;
184
193
 
185
- function installForgeStopHook(forgePort: number): void {
194
+ export function installForgeStopHook(forgePort: number): void {
186
195
  const settingsFile = join(homedir(), '.claude', 'settings.json');
187
196
  const now = new Date();
188
197
  const dateStr = `${now.getFullYear()}${String(now.getMonth()+1).padStart(2,'0')}${String(now.getDate()).padStart(2,'0')}-${String(now.getHours()).padStart(2,'0')}${String(now.getMinutes()).padStart(2,'0')}`;
189
198
  const backupFile = join(homedir(), '.claude', `settings.json.forge-backup-${dateStr}`);
190
199
  const daemonPort = forgePort + 2; // 8403 → 8405
191
200
 
192
- // Hook reads agent context from .forge/agent-context.json in the project dir.
193
- // This file is written by ensurePersistentSession for each agent's workDir.
194
- // Falls back to env vars if file doesn't exist.
195
- const hookCommand = `${FORGE_HOOK_MARKER}\nCTX_FILE="$(pwd)/.forge/agent-context.json"; if [ -f "$CTX_FILE" ]; then WS_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('workspaceId',''))" 2>/dev/null); AG_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('agentId',''))" 2>/dev/null); elif [ -n "$FORGE_WORKSPACE_ID" ]; then WS_ID="$FORGE_WORKSPACE_ID"; AG_ID="$FORGE_AGENT_ID"; fi; if [ -n "$WS_ID" ] && [ -n "$AG_ID" ]; then curl -s -X POST "http://localhost:${daemonPort}/workspace/$WS_ID/agents" -H "Content-Type: application/json" -d "{\\"action\\":\\"agent_done\\",\\"agentId\\":\\"$AG_ID\\"}" > /dev/null 2>&1 & fi`;
201
+ // Hook reads context from .forge/ in the project dir ($(pwd)).
202
+ // agent-context.json workspace smith completion (written by ensurePersistentSession)
203
+ // task-context.json → tmux task completion (written by task-tmux-backend)
204
+ // Falls back to env vars for workspace if file doesn't exist.
205
+ const hookCommand = `${FORGE_HOOK_MARKER}\nCTX_FILE="$(pwd)/.forge/agent-context.json"; TASK_FILE="$(pwd)/.forge/task-context.json"; if [ -f "$CTX_FILE" ]; then WS_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('workspaceId',''))" 2>/dev/null); AG_ID=$(python3 -c "import json;print(json.load(open('$CTX_FILE')).get('agentId',''))" 2>/dev/null); elif [ -n "$FORGE_WORKSPACE_ID" ]; then WS_ID="$FORGE_WORKSPACE_ID"; AG_ID="$FORGE_AGENT_ID"; fi; if [ -n "$WS_ID" ] && [ -n "$AG_ID" ]; then curl -s -X POST "http://localhost:${daemonPort}/workspace/$WS_ID/agents" -H "Content-Type: application/json" -d "{\\"action\\":\\"agent_done\\",\\"agentId\\":\\"$AG_ID\\"}" > /dev/null 2>&1 & fi; if [ -f "$TASK_FILE" ]; then T_ID=$(python3 -c "import json;print(json.load(open('$TASK_FILE')).get('taskId',''))" 2>/dev/null); T_PORT=$(python3 -c "import json;print(json.load(open('$TASK_FILE')).get('port','8403'))" 2>/dev/null); if [ -n "$T_ID" ]; then curl -s -X POST "http://localhost:$T_PORT/api/tasks/$T_ID/hook/stop" > /dev/null 2>&1 & fi; fi`;
196
206
 
197
207
  try {
198
208
  let settings: any = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.79",
3
+ "version": "0.10.81",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {
package/proxy.ts CHANGED
@@ -38,10 +38,11 @@ export const proxy = auth((req) => {
38
38
  return NextResponse.next();
39
39
  }
40
40
 
41
- // /api/connector-tool loopback-only, no auth (used by Forge-internal
42
- // callers: pipelines via curl, jobs scheduler, CLI). Non-loopback hosts
43
- // fall through to the normal check.
44
- if (pathname === '/api/connector-tool') {
41
+ // Loopback-only routes: no auth required when called from localhost.
42
+ // Used by Forge-internal callers: pipelines via curl, jobs scheduler, CLI.
43
+ // Non-loopback hosts fall through to the normal auth check.
44
+ const loopbackOnly = ['/api/connector-tool', '/api/tasks'];
45
+ if (loopbackOnly.some(p => pathname === p || pathname.startsWith(p + '/'))) {
45
46
  const host = req.headers.get('host') || '';
46
47
  if (host.startsWith('127.0.0.1:') || host.startsWith('localhost:')) {
47
48
  return NextResponse.next();
@@ -249,6 +249,7 @@ function initSchema(db: Database.Database) {
249
249
  migrate('ALTER TABLE project_pipelines ADD COLUMN last_run_at TEXT');
250
250
  migrate('ALTER TABLE pipeline_runs ADD COLUMN dedup_key TEXT');
251
251
  migrate("ALTER TABLE tasks ADD COLUMN agent TEXT DEFAULT 'claude'");
252
+ migrate('ALTER TABLE tasks ADD COLUMN backend TEXT DEFAULT NULL');
252
253
 
253
254
  // Unique index for dedup (needs pipeline_runs to exist; only applies when dedup_key is NOT NULL).
254
255
  try { db.exec('CREATE UNIQUE INDEX IF NOT EXISTS idx_pipeline_runs_dedup ON pipeline_runs(project_path, workflow_name, dedup_key)'); } catch {}
@@ -110,6 +110,9 @@ export interface Task {
110
110
  completedAt?: string;
111
111
  scheduledAt?: string;
112
112
  agent?: string;
113
+ /** 'tmux' = run inside a dedicated tmux session (subscription billing).
114
+ * Omitted / undefined = default headless spawn. */
115
+ backend?: 'tmux';
113
116
  // Lite-list metadata: present in /api/tasks responses, undefined in detail
114
117
  logSize?: number;
115
118
  hasGitDiff?: boolean;