@jive-ai/cli 0.0.30 → 0.0.32
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/README.md +109 -0
- package/dist/graphql-client-DtLFKd4m.mjs +3 -0
- package/dist/index.mjs +456 -115
- package/package.json +8 -5
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Jive CLI
|
|
2
|
+
|
|
3
|
+
The official CLI for Jive AI - autonomous development workflows powered by Claude.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### npm
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @jive-ai/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Docker
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
docker pull jive-ai/task:latest
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
### Environment Variables
|
|
22
|
+
|
|
23
|
+
The CLI can be configured using environment variables:
|
|
24
|
+
|
|
25
|
+
- `JIVE_TASK_IDLE_TIMEOUT` - Minutes of idle time before a task automatically terminates (default: 5). Tasks are ephemeral and will gracefully shutdown after this period of inactivity, freeing up runner capacity. Session data is preserved for resume.
|
|
26
|
+
- Development: `JIVE_TASK_IDLE_TIMEOUT=10` (10 minutes - more forgiving)
|
|
27
|
+
- Production: `JIVE_TASK_IDLE_TIMEOUT=5` (5 minutes - default)
|
|
28
|
+
- CI/CD: `JIVE_TASK_IDLE_TIMEOUT=30` (30 minutes - long builds)
|
|
29
|
+
|
|
30
|
+
- `JIVE_API_KEY` - Your Jive API authentication key (required for task runners)
|
|
31
|
+
- `JIVE_TEAM_ID` - The team ID to run tasks for (required for task runners)
|
|
32
|
+
- `JIVE_API_URL` - API endpoint (defaults to production)
|
|
33
|
+
- `JIVE_WS_URL` - WebSocket endpoint (defaults to production)
|
|
34
|
+
- `JIVE_LOG_FILE` - MCP server log path (defaults to `/tmp/jive-mcp.log`)
|
|
35
|
+
|
|
36
|
+
## Docker Image
|
|
37
|
+
|
|
38
|
+
The docker image is used for each task process container. Whenever you spin up a new task in Jive,
|
|
39
|
+
it will start a new container with this image. You can specify your own custom image in the project settings
|
|
40
|
+
as long as the same entrypoint script is provided. The task runner relies on it.
|
|
41
|
+
|
|
42
|
+
### Building Your Own Images
|
|
43
|
+
|
|
44
|
+
You can extend the Jive task runner image to add custom tools or dependencies:
|
|
45
|
+
|
|
46
|
+
```dockerfile
|
|
47
|
+
FROM jive-ai/task:latest
|
|
48
|
+
|
|
49
|
+
# Add your custom dependencies
|
|
50
|
+
RUN apk add --no-cache python3 py3-pip
|
|
51
|
+
|
|
52
|
+
# Install additional tools
|
|
53
|
+
RUN pip install --break-system-packages black ruff
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Publishing the Docker Image (Maintainers)
|
|
57
|
+
|
|
58
|
+
### One-Time Setup
|
|
59
|
+
|
|
60
|
+
1. Login to Docker Hub:
|
|
61
|
+
```bash
|
|
62
|
+
docker login
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
2. Create a buildx builder for multi-platform builds:
|
|
66
|
+
```bash
|
|
67
|
+
docker buildx create --name mybuilder --use
|
|
68
|
+
docker buildx inspect --bootstrap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Publishing a New Version
|
|
72
|
+
|
|
73
|
+
After publishing a new version to npm, publish the corresponding Docker image:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cd cli
|
|
77
|
+
npm run docker:publish
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This will:
|
|
81
|
+
- Build for both `linux/amd64` and `linux/arm64` platforms
|
|
82
|
+
- Tag with both `latest` and the current package version (e.g., `0.0.31`)
|
|
83
|
+
- Push both tags to Docker Hub
|
|
84
|
+
|
|
85
|
+
The image pulls the CLI from npm (using the `@jive-ai/cli` package), so make sure you've published to npm first.
|
|
86
|
+
|
|
87
|
+
### Manual Steps
|
|
88
|
+
|
|
89
|
+
If you need more control:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# Build only (no push)
|
|
93
|
+
npm run docker:build:public
|
|
94
|
+
|
|
95
|
+
# Push to Docker Hub
|
|
96
|
+
npm run docker:push
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
### Local Development with Docker
|
|
102
|
+
|
|
103
|
+
To test the Docker image with your local code changes:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run docker:build:local
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This builds an image using the local CLI tarball instead of pulling from npm.
|
package/dist/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import crypto from "crypto";
|
|
|
14
14
|
import "gray-matter";
|
|
15
15
|
import WebSocket from "ws";
|
|
16
16
|
import { decode, encode } from "js-base64";
|
|
17
|
+
import { GraphQLClient } from "graphql-request";
|
|
17
18
|
import { createSdkMcpServer, query, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
18
19
|
import { z } from "zod";
|
|
19
20
|
import dedent from "dedent";
|
|
@@ -154,9 +155,9 @@ async function isProjectInitialized() {
|
|
|
154
155
|
|
|
155
156
|
//#endregion
|
|
156
157
|
//#region src/constants.ts
|
|
157
|
-
const API_URL = process.env.JIVE_API_URL || "
|
|
158
|
-
const GRAPHQL_API_URL = process.env.JIVE_GRAPHQL_API_URL || "
|
|
159
|
-
const WS_URL = process.env.JIVE_WS_URL || "
|
|
158
|
+
const API_URL = process.env.JIVE_API_URL || "https://getjive.app";
|
|
159
|
+
const GRAPHQL_API_URL = process.env.JIVE_GRAPHQL_API_URL || "https://api.getjive.app/graphql";
|
|
160
|
+
const WS_URL = process.env.JIVE_WS_URL || "wss://api.getjive.app";
|
|
160
161
|
|
|
161
162
|
//#endregion
|
|
162
163
|
//#region src/commands/auth.ts
|
|
@@ -2337,12 +2338,13 @@ const queries = {
|
|
|
2337
2338
|
}
|
|
2338
2339
|
`),
|
|
2339
2340
|
GetSessionLines: graphql(`
|
|
2340
|
-
query GetSessionLines($taskId: ID!, $sessionId: ID!, $first: Int, $after: String) {
|
|
2341
|
-
sessionLines(taskId: $taskId, sessionId: $sessionId, first: $first, after: $after) {
|
|
2341
|
+
query GetSessionLines($taskId: ID!, $sessionId: ID!, $first: Int, $after: String, $excludePending: Boolean) {
|
|
2342
|
+
sessionLines(taskId: $taskId, sessionId: $sessionId, first: $first, after: $after, excludePending: $excludePending) {
|
|
2342
2343
|
edges {
|
|
2343
2344
|
node {
|
|
2344
2345
|
uuid
|
|
2345
2346
|
schema
|
|
2347
|
+
status
|
|
2346
2348
|
payload
|
|
2347
2349
|
createdAt
|
|
2348
2350
|
}
|
|
@@ -2404,9 +2406,9 @@ const mutations = {
|
|
|
2404
2406
|
}
|
|
2405
2407
|
}
|
|
2406
2408
|
`),
|
|
2407
|
-
|
|
2408
|
-
mutation
|
|
2409
|
-
|
|
2409
|
+
PromptSendToAgent: graphql(`
|
|
2410
|
+
mutation PromptSendToAgent($input: PromptSendToAgentInput!) {
|
|
2411
|
+
promptSendToAgent(input: $input) {
|
|
2410
2412
|
line {
|
|
2411
2413
|
uuid
|
|
2412
2414
|
schema
|
|
@@ -2433,34 +2435,6 @@ const mutations = {
|
|
|
2433
2435
|
}
|
|
2434
2436
|
}
|
|
2435
2437
|
}
|
|
2436
|
-
`),
|
|
2437
|
-
RefreshPreview: graphql(`
|
|
2438
|
-
mutation RefreshPreview($input: RefreshPreviewInput!) {
|
|
2439
|
-
refreshPreview(input: $input) {
|
|
2440
|
-
task {
|
|
2441
|
-
id
|
|
2442
|
-
previewUrl
|
|
2443
|
-
}
|
|
2444
|
-
errors {
|
|
2445
|
-
message
|
|
2446
|
-
code
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
`),
|
|
2451
|
-
AddressReview: graphql(`
|
|
2452
|
-
mutation AddressReview($input: AddressReviewInput!) {
|
|
2453
|
-
addressReview(input: $input) {
|
|
2454
|
-
task {
|
|
2455
|
-
id
|
|
2456
|
-
status
|
|
2457
|
-
}
|
|
2458
|
-
errors {
|
|
2459
|
-
message
|
|
2460
|
-
code
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
2438
|
`),
|
|
2465
2439
|
CreatePlan: graphql(`
|
|
2466
2440
|
mutation CreatePlan($input: CreatePlanInput!) {
|
|
@@ -2565,6 +2539,21 @@ const cliQueries = {
|
|
|
2565
2539
|
createdAt
|
|
2566
2540
|
}
|
|
2567
2541
|
}
|
|
2542
|
+
`),
|
|
2543
|
+
Team: graphql(`
|
|
2544
|
+
query Team($id: ID!) {
|
|
2545
|
+
team(id: $id) {
|
|
2546
|
+
id
|
|
2547
|
+
name
|
|
2548
|
+
settings {
|
|
2549
|
+
claude {
|
|
2550
|
+
defaultModel
|
|
2551
|
+
environmentVariables
|
|
2552
|
+
betaFlags
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2568
2557
|
`),
|
|
2569
2558
|
InterruptedTasks: graphql(`
|
|
2570
2559
|
query InterruptedTasks($runnerId: ID!, $limit: Int) {
|
|
@@ -2577,7 +2566,13 @@ const cliQueries = {
|
|
|
2577
2566
|
directory
|
|
2578
2567
|
repository
|
|
2579
2568
|
branch
|
|
2580
|
-
gitAuth
|
|
2569
|
+
gitAuth {
|
|
2570
|
+
privateKey
|
|
2571
|
+
repositoryUrl
|
|
2572
|
+
provider
|
|
2573
|
+
method
|
|
2574
|
+
}
|
|
2575
|
+
permissionMode
|
|
2581
2576
|
}
|
|
2582
2577
|
sessionId
|
|
2583
2578
|
}
|
|
@@ -2616,6 +2611,9 @@ const cliQueries = {
|
|
|
2616
2611
|
description
|
|
2617
2612
|
gitRepoUrl
|
|
2618
2613
|
gitDefaultBranch
|
|
2614
|
+
team {
|
|
2615
|
+
id
|
|
2616
|
+
}
|
|
2619
2617
|
createdAt
|
|
2620
2618
|
updatedAt
|
|
2621
2619
|
}
|
|
@@ -2763,6 +2761,7 @@ async function getGraphQLClient() {
|
|
|
2763
2761
|
function mapStatusToGraphQL(dbStatus) {
|
|
2764
2762
|
return {
|
|
2765
2763
|
"draft": "DRAFT",
|
|
2764
|
+
"pending": "PENDING",
|
|
2766
2765
|
"planning": "PLANNING",
|
|
2767
2766
|
"planned": "PLANNED",
|
|
2768
2767
|
"in-progress": "IN_PROGRESS",
|
|
@@ -2951,10 +2950,11 @@ var ApiClient = class {
|
|
|
2951
2950
|
} : null
|
|
2952
2951
|
};
|
|
2953
2952
|
}
|
|
2954
|
-
async getSessionLines(taskId, sessionId) {
|
|
2953
|
+
async getSessionLines(taskId, sessionId, excludePending = false) {
|
|
2955
2954
|
return (await (await getGraphQLClient()).request(queries.GetSessionLines, {
|
|
2956
2955
|
taskId: String(taskId),
|
|
2957
|
-
sessionId: String(sessionId)
|
|
2956
|
+
sessionId: String(sessionId),
|
|
2957
|
+
excludePending
|
|
2958
2958
|
})).sessionLines.edges.map((edge) => ({
|
|
2959
2959
|
uuid: edge.node.uuid,
|
|
2960
2960
|
schema: edge.node.schema,
|
|
@@ -3269,15 +3269,42 @@ async function fetchProject(teamId) {
|
|
|
3269
3269
|
|
|
3270
3270
|
//#endregion
|
|
3271
3271
|
//#region src/runner/taskUtils.ts
|
|
3272
|
+
const ProjectResourceLimitsQuery = graphql(`
|
|
3273
|
+
query ProjectResourceLimits($projectId: ID!) {
|
|
3274
|
+
project(id: $projectId) {
|
|
3275
|
+
id
|
|
3276
|
+
dockerCpus
|
|
3277
|
+
dockerMemory
|
|
3278
|
+
dockerImage
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
`);
|
|
3282
|
+
async function fetchProjectResourceLimits(projectId) {
|
|
3283
|
+
try {
|
|
3284
|
+
const result = await (await getGraphQLClient()).request(ProjectResourceLimitsQuery, { projectId: projectId.toString() });
|
|
3285
|
+
if (!result.project) {
|
|
3286
|
+
console.log(chalk.yellow(`Warning: Could not fetch project ${projectId} resource limits`));
|
|
3287
|
+
return;
|
|
3288
|
+
}
|
|
3289
|
+
return {
|
|
3290
|
+
dockerCpus: result.project.dockerCpus,
|
|
3291
|
+
dockerMemory: result.project.dockerMemory,
|
|
3292
|
+
dockerImage: result.project.dockerImage
|
|
3293
|
+
};
|
|
3294
|
+
} catch (error$1) {
|
|
3295
|
+
console.log(chalk.yellow(`Warning: Failed to fetch project resource limits: ${error$1.message}`));
|
|
3296
|
+
return;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
3272
3299
|
function spawnTask(ctx, opts) {
|
|
3273
3300
|
throw new Error("Shell executor not supported anymore.");
|
|
3274
3301
|
}
|
|
3275
|
-
async function spawnTaskDocker(ctx, opts) {
|
|
3302
|
+
async function spawnTaskDocker(ctx, config$2, opts) {
|
|
3276
3303
|
mkdirSync(ctx.directory, { recursive: true });
|
|
3304
|
+
const limits = getEffectiveResourceLimits(config$2, await fetchProjectResourceLimits(ctx.projectId));
|
|
3277
3305
|
const envVars = [];
|
|
3278
3306
|
const credentials = await getCredentials();
|
|
3279
3307
|
if (!credentials) throw new Error("No credentials found");
|
|
3280
|
-
const config$2 = await getRunnerConfig();
|
|
3281
3308
|
envVars.push("-e", `JIVE_API_KEY=${credentials.token}`);
|
|
3282
3309
|
envVars.push("-e", `ANTHROPIC_API_KEY=${credentials.anthropicApiKey}`);
|
|
3283
3310
|
envVars.push("-e", `JIVE_TEAM_ID=${config$2.teamId}`);
|
|
@@ -3311,7 +3338,8 @@ async function spawnTaskDocker(ctx, opts) {
|
|
|
3311
3338
|
}
|
|
3312
3339
|
dockerArgs.push("--network", "bridge");
|
|
3313
3340
|
if (isDevMode) dockerArgs.push("--add-host", "host.docker.internal:host-gateway");
|
|
3314
|
-
dockerArgs.push("--cpus",
|
|
3341
|
+
dockerArgs.push("--cpus", limits.docker.cpus, "--memory", limits.docker.memory, "-v", `${ctx.directory}:/workspace`, "-w", "/workspace", ...envVars, limits.docker.image, encode(JSON.stringify(ctx)));
|
|
3342
|
+
console.log(chalk.dim(`[Task ${ctx.taskId}] Spawning Docker container with args:`, JSON.stringify(dockerArgs, null, 2)));
|
|
3315
3343
|
const taskProcess = spawn("docker", dockerArgs, { cwd: ctx.directory });
|
|
3316
3344
|
let buffer = "";
|
|
3317
3345
|
taskProcess.stdout.on("data", (data) => {
|
|
@@ -3352,21 +3380,49 @@ async function spawnTaskDocker(ctx, opts) {
|
|
|
3352
3380
|
return taskProcess;
|
|
3353
3381
|
}
|
|
3354
3382
|
|
|
3383
|
+
//#endregion
|
|
3384
|
+
//#region src/runner/graphql-client.ts
|
|
3385
|
+
async function createGraphQLClient() {
|
|
3386
|
+
const credentials = await getCredentials();
|
|
3387
|
+
if (!credentials?.token) throw new Error("No credentials found, run `jive login` first");
|
|
3388
|
+
const client = new GraphQLClient(GRAPHQL_API_URL, { headers: { "X-API-Key": credentials.token } });
|
|
3389
|
+
return {
|
|
3390
|
+
async request(document, variables) {
|
|
3391
|
+
return client.request(document, variables);
|
|
3392
|
+
},
|
|
3393
|
+
async rawRequest(document, variables) {
|
|
3394
|
+
return client.request(document, variables);
|
|
3395
|
+
}
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3355
3399
|
//#endregion
|
|
3356
3400
|
//#region src/runner/index.ts
|
|
3357
3401
|
const execAsync$3 = promisify(exec);
|
|
3358
|
-
const
|
|
3359
|
-
|
|
3402
|
+
const PENDING_TASK_POLL_INTERVAL = 3e4;
|
|
3403
|
+
function getEffectiveResourceLimits(config$2, projectLimits) {
|
|
3404
|
+
const maxConcurrentTasksEnv = process.env.JIVE_MAX_CONCURRENT_TASKS;
|
|
3405
|
+
return {
|
|
3406
|
+
maxConcurrentTasks: maxConcurrentTasksEnv && !isNaN(parseInt(maxConcurrentTasksEnv)) ? parseInt(maxConcurrentTasksEnv) : config$2.resourceLimits?.maxConcurrentTasks ?? 5,
|
|
3407
|
+
docker: {
|
|
3408
|
+
cpus: projectLimits?.dockerCpus?.toString() || process.env.JIVE_DOCKER_CPUS || config$2.resourceLimits?.docker?.cpus || "2",
|
|
3409
|
+
memory: projectLimits?.dockerMemory || process.env.JIVE_DOCKER_MEMORY || config$2.resourceLimits?.docker?.memory || "2g",
|
|
3410
|
+
image: projectLimits?.dockerImage || process.env.JIVE_DOCKER_IMAGE || config$2.resourceLimits?.docker?.image || "jive-ai/task:latest"
|
|
3411
|
+
}
|
|
3412
|
+
};
|
|
3413
|
+
}
|
|
3360
3414
|
var TaskRunner = class {
|
|
3361
3415
|
config;
|
|
3362
3416
|
ws = null;
|
|
3363
3417
|
heartbeatInterval = null;
|
|
3364
|
-
|
|
3418
|
+
pendingTaskPollInterval = null;
|
|
3365
3419
|
isRunning = false;
|
|
3366
3420
|
reconnectTimeout = null;
|
|
3421
|
+
maxConcurrentTasks;
|
|
3367
3422
|
taskProcesses = {};
|
|
3368
3423
|
constructor(config$2) {
|
|
3369
3424
|
this.config = config$2;
|
|
3425
|
+
this.maxConcurrentTasks = getEffectiveResourceLimits(config$2).maxConcurrentTasks;
|
|
3370
3426
|
}
|
|
3371
3427
|
async start() {
|
|
3372
3428
|
this.isRunning = true;
|
|
@@ -3384,7 +3440,7 @@ var TaskRunner = class {
|
|
|
3384
3440
|
clearInterval(this.heartbeatInterval);
|
|
3385
3441
|
this.heartbeatInterval = null;
|
|
3386
3442
|
}
|
|
3387
|
-
this.
|
|
3443
|
+
this.stopPendingTaskPolling();
|
|
3388
3444
|
if (this.reconnectTimeout) {
|
|
3389
3445
|
clearTimeout(this.reconnectTimeout);
|
|
3390
3446
|
this.reconnectTimeout = null;
|
|
@@ -3426,12 +3482,75 @@ var TaskRunner = class {
|
|
|
3426
3482
|
}
|
|
3427
3483
|
});
|
|
3428
3484
|
}
|
|
3429
|
-
|
|
3430
|
-
|
|
3485
|
+
async fetchTaskContext(taskId) {
|
|
3486
|
+
try {
|
|
3487
|
+
const { getGraphQLClient: getGraphQLClient$1 } = await import("./graphql-client-DtLFKd4m.mjs");
|
|
3488
|
+
const client = await getGraphQLClient$1();
|
|
3489
|
+
const query$1 = graphql(`
|
|
3490
|
+
query TaskContext($taskId: ID!) {
|
|
3491
|
+
taskContext(taskId: $taskId) {
|
|
3492
|
+
taskId
|
|
3493
|
+
sessionId
|
|
3494
|
+
claudeSessionId
|
|
3495
|
+
projectId
|
|
3496
|
+
directory
|
|
3497
|
+
repository
|
|
3498
|
+
branch
|
|
3499
|
+
gitAuth {
|
|
3500
|
+
privateKey
|
|
3501
|
+
repositoryUrl
|
|
3502
|
+
provider
|
|
3503
|
+
method
|
|
3504
|
+
}
|
|
3505
|
+
permissionMode
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
`);
|
|
3509
|
+
const data = await client.request(query$1, { taskId: taskId.toString() });
|
|
3510
|
+
if (!data.taskContext) {
|
|
3511
|
+
console.error(chalk.red(`No context found for task ${taskId}`));
|
|
3512
|
+
return null;
|
|
3513
|
+
}
|
|
3514
|
+
const auth = (() => {
|
|
3515
|
+
const gitAuth = data.taskContext.gitAuth;
|
|
3516
|
+
if (!gitAuth) return null;
|
|
3517
|
+
return {
|
|
3518
|
+
...gitAuth,
|
|
3519
|
+
provider: gitAuth.provider,
|
|
3520
|
+
method: gitAuth.method
|
|
3521
|
+
};
|
|
3522
|
+
})();
|
|
3523
|
+
return {
|
|
3524
|
+
taskId: data.taskContext.taskId,
|
|
3525
|
+
sessionId: data.taskContext.sessionId,
|
|
3526
|
+
claudeSessionId: data.taskContext.claudeSessionId,
|
|
3527
|
+
projectId: data.taskContext.projectId,
|
|
3528
|
+
directory: data.taskContext.directory,
|
|
3529
|
+
repository: data.taskContext.repository,
|
|
3530
|
+
branch: data.taskContext.branch,
|
|
3531
|
+
gitAuth: auth || void 0,
|
|
3532
|
+
permissionMode: data.taskContext.permissionMode
|
|
3533
|
+
};
|
|
3534
|
+
} catch (error$1) {
|
|
3535
|
+
console.error(error$1);
|
|
3536
|
+
console.error(chalk.red(`Failed to fetch task context: ${error$1.message}`));
|
|
3537
|
+
return null;
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
async fetchTaskProcess(taskId) {
|
|
3541
|
+
const existingProcess = this.taskProcesses[taskId.toString()];
|
|
3431
3542
|
if (existingProcess) return existingProcess;
|
|
3432
|
-
if (Object.values(this.taskProcesses).filter((p$1) => p$1 !== null).length >=
|
|
3433
|
-
|
|
3434
|
-
|
|
3543
|
+
if (Object.values(this.taskProcesses).filter((p$1) => p$1 !== null).length >= this.maxConcurrentTasks) {
|
|
3544
|
+
console.warn(chalk.yellow(`Maximum concurrent tasks reached (${this.maxConcurrentTasks})`));
|
|
3545
|
+
return null;
|
|
3546
|
+
}
|
|
3547
|
+
const ctx = await this.fetchTaskContext(taskId);
|
|
3548
|
+
if (!ctx) {
|
|
3549
|
+
console.error(chalk.red(`Failed to fetch context for task ${taskId}`));
|
|
3550
|
+
return null;
|
|
3551
|
+
}
|
|
3552
|
+
return this.taskProcesses[taskId.toString()] = new Promise((resolve) => {
|
|
3553
|
+
(this.config.type === "docker" ? spawnTaskDocker(ctx, this.config, {
|
|
3435
3554
|
onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
|
|
3436
3555
|
onError: () => {
|
|
3437
3556
|
this.taskProcesses[ctx.taskId.toString()] = null;
|
|
@@ -3439,11 +3558,29 @@ var TaskRunner = class {
|
|
|
3439
3558
|
onClose: () => {
|
|
3440
3559
|
this.taskProcesses[ctx.taskId.toString()] = null;
|
|
3441
3560
|
}
|
|
3442
|
-
})
|
|
3561
|
+
}) : Promise.resolve(spawnTask(ctx, {
|
|
3562
|
+
onMessage: (message) => this.onTaskMessage(ctx.taskId, message),
|
|
3563
|
+
onError: () => {
|
|
3564
|
+
this.taskProcesses[ctx.taskId.toString()] = null;
|
|
3565
|
+
},
|
|
3566
|
+
onClose: () => {
|
|
3567
|
+
this.taskProcesses[ctx.taskId.toString()] = null;
|
|
3568
|
+
}
|
|
3569
|
+
}))).then((process$1) => {
|
|
3570
|
+
resolve({
|
|
3571
|
+
process: process$1,
|
|
3572
|
+
ctx
|
|
3573
|
+
});
|
|
3574
|
+
});
|
|
3443
3575
|
});
|
|
3444
3576
|
}
|
|
3445
|
-
async sendToTask(
|
|
3446
|
-
|
|
3577
|
+
async sendToTask(taskId, message) {
|
|
3578
|
+
const result = await this.fetchTaskProcess(taskId);
|
|
3579
|
+
if (!result) {
|
|
3580
|
+
console.warn(chalk.yellow(`Cannot write message for task ${taskId}, process not found or unobtainable`));
|
|
3581
|
+
return;
|
|
3582
|
+
}
|
|
3583
|
+
result.process.stdin?.write(JSON.stringify(message) + "\n");
|
|
3447
3584
|
}
|
|
3448
3585
|
async connect() {
|
|
3449
3586
|
console.log(chalk.dim(`Connecting to ${WS_URL}...`));
|
|
@@ -3475,7 +3612,7 @@ var TaskRunner = class {
|
|
|
3475
3612
|
process.exit(1);
|
|
3476
3613
|
}
|
|
3477
3614
|
this.startHeartbeat();
|
|
3478
|
-
this.
|
|
3615
|
+
this.startPendingTaskPolling();
|
|
3479
3616
|
this.send({
|
|
3480
3617
|
type: "runner_connected",
|
|
3481
3618
|
payload: {
|
|
@@ -3518,7 +3655,20 @@ var TaskRunner = class {
|
|
|
3518
3655
|
});
|
|
3519
3656
|
break;
|
|
3520
3657
|
case "task_message":
|
|
3521
|
-
|
|
3658
|
+
const { taskId, message: taskMsg } = message.payload;
|
|
3659
|
+
const messageId = message.payload.messageId;
|
|
3660
|
+
const activeCount = Object.values(this.taskProcesses).filter((p$1) => p$1 !== null).length;
|
|
3661
|
+
const accepted = activeCount < this.maxConcurrentTasks;
|
|
3662
|
+
if (messageId) this.send({
|
|
3663
|
+
type: "task_ack",
|
|
3664
|
+
payload: {
|
|
3665
|
+
messageId,
|
|
3666
|
+
accepted,
|
|
3667
|
+
reason: accepted ? void 0 : "capacity_full"
|
|
3668
|
+
}
|
|
3669
|
+
});
|
|
3670
|
+
if (accepted) this.sendToTask(taskId, taskMsg);
|
|
3671
|
+
else console.warn(chalk.yellow(`Rejected task ${taskId} - at capacity (${activeCount}/${this.maxConcurrentTasks})`));
|
|
3522
3672
|
break;
|
|
3523
3673
|
default: console.warn(chalk.yellow(`Unable to handle message:\n\n${JSON.stringify(message, null, 2)}`));
|
|
3524
3674
|
}
|
|
@@ -3532,7 +3682,7 @@ var TaskRunner = class {
|
|
|
3532
3682
|
clearInterval(this.heartbeatInterval);
|
|
3533
3683
|
this.heartbeatInterval = null;
|
|
3534
3684
|
}
|
|
3535
|
-
this.
|
|
3685
|
+
this.stopPendingTaskPolling();
|
|
3536
3686
|
if (this.isRunning) {
|
|
3537
3687
|
console.log(chalk.dim("Attempting to reconnect in 5 seconds..."));
|
|
3538
3688
|
this.reconnectTimeout = setTimeout(() => {
|
|
@@ -3549,59 +3699,131 @@ var TaskRunner = class {
|
|
|
3549
3699
|
type: "heartbeat",
|
|
3550
3700
|
payload: { timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
3551
3701
|
});
|
|
3552
|
-
},
|
|
3702
|
+
}, 5e3);
|
|
3553
3703
|
}
|
|
3554
|
-
|
|
3555
|
-
this.
|
|
3556
|
-
this.
|
|
3557
|
-
this.
|
|
3558
|
-
},
|
|
3704
|
+
startPendingTaskPolling() {
|
|
3705
|
+
this.pollForPendingTasks();
|
|
3706
|
+
this.pendingTaskPollInterval = setInterval(() => {
|
|
3707
|
+
this.pollForPendingTasks();
|
|
3708
|
+
}, PENDING_TASK_POLL_INTERVAL);
|
|
3559
3709
|
}
|
|
3560
|
-
|
|
3561
|
-
if (this.
|
|
3562
|
-
clearInterval(this.
|
|
3563
|
-
this.
|
|
3710
|
+
stopPendingTaskPolling() {
|
|
3711
|
+
if (this.pendingTaskPollInterval) {
|
|
3712
|
+
clearInterval(this.pendingTaskPollInterval);
|
|
3713
|
+
this.pendingTaskPollInterval = null;
|
|
3564
3714
|
}
|
|
3565
3715
|
}
|
|
3566
|
-
async
|
|
3567
|
-
|
|
3716
|
+
async pollForPendingTasks() {
|
|
3717
|
+
const activeCount = Object.values(this.taskProcesses).filter((p$1) => p$1 !== null).length;
|
|
3718
|
+
if (activeCount >= this.maxConcurrentTasks) {
|
|
3719
|
+
console.log(chalk.dim(`At capacity (${activeCount}/${this.maxConcurrentTasks}), skipping poll`));
|
|
3720
|
+
return;
|
|
3721
|
+
}
|
|
3722
|
+
const availableSlots = this.maxConcurrentTasks - activeCount;
|
|
3723
|
+
console.log(`Finding pending tasks, ${availableSlots} / ${this.maxConcurrentTasks} task slots available`);
|
|
3724
|
+
try {
|
|
3725
|
+
const client = await createGraphQLClient();
|
|
3726
|
+
const query$1 = graphql(`
|
|
3727
|
+
query PendingTasks($limit: Int!) {
|
|
3728
|
+
tasks(
|
|
3729
|
+
status: [PENDING]
|
|
3730
|
+
first: $limit
|
|
3731
|
+
) {
|
|
3732
|
+
edges {
|
|
3733
|
+
node {
|
|
3734
|
+
id
|
|
3735
|
+
title
|
|
3736
|
+
project { id team { id } }
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
`);
|
|
3742
|
+
const pendingTasks = (await client.request(query$1, { limit: availableSlots })).tasks.edges.map((e$3) => e$3.node);
|
|
3743
|
+
if (pendingTasks.length > 0) console.log(chalk.yellow(`Found ${pendingTasks.length} pending task(s) to pick up`));
|
|
3744
|
+
for (const task of pendingTasks) await this.pickupPendingTask(parseInt(task.id));
|
|
3745
|
+
} catch (error$1) {
|
|
3746
|
+
if (!error$1.message?.includes("Unable to connect")) console.error(chalk.red(`Failed to poll for pending tasks: ${error$1.message}`));
|
|
3747
|
+
}
|
|
3568
3748
|
}
|
|
3569
|
-
async
|
|
3570
|
-
console.log(chalk.yellow(`
|
|
3749
|
+
async pickupPendingTask(taskId) {
|
|
3750
|
+
console.log(chalk.yellow(`Picking up pending task ${taskId}...`));
|
|
3571
3751
|
try {
|
|
3572
|
-
const
|
|
3573
|
-
const
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3752
|
+
const client = await createGraphQLClient();
|
|
3753
|
+
const mutation = graphql(`
|
|
3754
|
+
mutation PickupPendingTask($taskId: ID!, $runnerId: ID!) {
|
|
3755
|
+
pickupPendingTask(taskId: $taskId, runnerId: $runnerId) {
|
|
3756
|
+
success
|
|
3757
|
+
task { id status }
|
|
3758
|
+
errors { message code }
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
`);
|
|
3762
|
+
const result = await client.request(mutation, {
|
|
3763
|
+
taskId: taskId.toString(),
|
|
3764
|
+
runnerId: this.config.id.toString()
|
|
3765
|
+
});
|
|
3766
|
+
if (result.pickupPendingTask.success) {
|
|
3767
|
+
console.log(chalk.green(`Successfully picked up task ${taskId}`));
|
|
3768
|
+
await this.fetchTaskProcess(taskId);
|
|
3769
|
+
} else {
|
|
3770
|
+
const error$1 = result.pickupPendingTask.errors?.[0];
|
|
3771
|
+
console.warn(chalk.yellow(`Failed to pickup task ${taskId}: ${error$1?.message || "unknown error"}`));
|
|
3772
|
+
}
|
|
3582
3773
|
} catch (error$1) {
|
|
3583
|
-
console.error(chalk.red(`Failed to
|
|
3774
|
+
console.error(chalk.red(`Failed to pickup task ${taskId}: ${error$1.message}`));
|
|
3584
3775
|
}
|
|
3585
3776
|
}
|
|
3586
3777
|
};
|
|
3587
3778
|
|
|
3779
|
+
//#endregion
|
|
3780
|
+
//#region src/runner/task/debugLog.ts
|
|
3781
|
+
function debugLog(message) {
|
|
3782
|
+
console.log(JSON.stringify({
|
|
3783
|
+
type: "debug_log",
|
|
3784
|
+
message
|
|
3785
|
+
}).replace(/\n/g, "\\n"));
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3588
3788
|
//#endregion
|
|
3589
3789
|
//#region src/runner/query.ts
|
|
3590
3790
|
async function queryClaude(prompt, mcpServer, opts) {
|
|
3591
|
-
const { sessionId, abortController, permissionMode = "
|
|
3791
|
+
const { sessionId, abortController, permissionMode = "BYPASS_PERMISSIONS", task } = opts;
|
|
3592
3792
|
const credentials = await getCredentials();
|
|
3593
3793
|
if (!credentials?.anthropicApiKey) throw new Error("Anthropic API key not found in credentials JSON");
|
|
3594
3794
|
process.env.ANTHROPIC_API_KEY = credentials.anthropicApiKey;
|
|
3595
|
-
|
|
3795
|
+
let teamSettings;
|
|
3796
|
+
try {
|
|
3797
|
+
const client = await getGraphQLClient();
|
|
3798
|
+
const projectData = await client.request(cliQueries.Project, { id: task.projectId.toString() });
|
|
3799
|
+
if (projectData.project?.team) {
|
|
3800
|
+
const claude = (await client.request(cliQueries.Team, { id: projectData.project.team.id })).team?.settings?.claude;
|
|
3801
|
+
if (claude) teamSettings = {
|
|
3802
|
+
defaultModel: claude.defaultModel,
|
|
3803
|
+
environmentVariables: claude.environmentVariables,
|
|
3804
|
+
betaFlags: claude.betaFlags
|
|
3805
|
+
};
|
|
3806
|
+
}
|
|
3807
|
+
} catch (error$1) {
|
|
3808
|
+
console.error("Failed to fetch team settings, using defaults:", error$1);
|
|
3809
|
+
}
|
|
3810
|
+
const defaultModel = teamSettings?.defaultModel || "sonnet";
|
|
3811
|
+
const environmentVariables = teamSettings?.environmentVariables || {};
|
|
3812
|
+
const betaFlags = teamSettings?.betaFlags || [];
|
|
3813
|
+
for (const [key, value$1] of Object.entries(environmentVariables)) process.env[key] = value$1;
|
|
3814
|
+
debugLog(`[Task ${task.id}] Querying Claude with permission mode: ${permissionMode}`);
|
|
3815
|
+
debugLog(`[Task ${task.id}] Using model: ${defaultModel}`);
|
|
3816
|
+
if (betaFlags.length > 0) debugLog(`[Task ${task.id}] Beta flags: ${betaFlags.join(", ")}`);
|
|
3596
3817
|
return query({
|
|
3597
3818
|
prompt,
|
|
3598
3819
|
options: {
|
|
3599
3820
|
abortController,
|
|
3600
3821
|
resume: sessionId,
|
|
3601
|
-
model:
|
|
3822
|
+
model: defaultModel,
|
|
3602
3823
|
includePartialMessages: false,
|
|
3603
3824
|
mcpServers: { "jive-tasks": mcpServer },
|
|
3604
|
-
permissionMode,
|
|
3825
|
+
permissionMode: mapPermissionMode(permissionMode),
|
|
3826
|
+
...betaFlags.length > 0 && { betas: betaFlags },
|
|
3605
3827
|
canUseTool: async (toolName, input) => {
|
|
3606
3828
|
if (toolName === "AskUserQuestion") {
|
|
3607
3829
|
const result = await task.requestUserQuestions(input);
|
|
@@ -3629,6 +3851,15 @@ async function queryClaude(prompt, mcpServer, opts) {
|
|
|
3629
3851
|
}
|
|
3630
3852
|
});
|
|
3631
3853
|
}
|
|
3854
|
+
function mapPermissionMode(permissionMode) {
|
|
3855
|
+
switch (permissionMode) {
|
|
3856
|
+
case "DEFAULT": return "default";
|
|
3857
|
+
case "BYPASS_PERMISSIONS": return "bypassPermissions";
|
|
3858
|
+
default:
|
|
3859
|
+
console.error(`[Task] Invalid permission mode: ${permissionMode}`);
|
|
3860
|
+
return;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3632
3863
|
|
|
3633
3864
|
//#endregion
|
|
3634
3865
|
//#region src/runner/TasksMCPServer.ts
|
|
@@ -3866,6 +4097,7 @@ function createTasksSdkServer(task) {
|
|
|
3866
4097
|
type: "text",
|
|
3867
4098
|
text: `Failed to set up cloudflare tunnel, error: ${result.error}`
|
|
3868
4099
|
}] };
|
|
4100
|
+
task.disableIdleTimeout();
|
|
3869
4101
|
await setTaskUrl(args.taskId, `https://${result.host}`);
|
|
3870
4102
|
return { content: [{
|
|
3871
4103
|
type: "text",
|
|
@@ -4013,6 +4245,11 @@ var Task = class {
|
|
|
4013
4245
|
tunnel = null;
|
|
4014
4246
|
fileWatcher = null;
|
|
4015
4247
|
lastFilePosition = 0;
|
|
4248
|
+
currentPendingLineUuid = null;
|
|
4249
|
+
idleTimeoutMs;
|
|
4250
|
+
lastActivityTime = Date.now();
|
|
4251
|
+
idleWatchdogInterval = null;
|
|
4252
|
+
idleTimeoutDisabled = false;
|
|
4016
4253
|
get defaultBranch() {
|
|
4017
4254
|
const tasksConfig = getTasksConfigSync();
|
|
4018
4255
|
if (!tasksConfig) {
|
|
@@ -4021,16 +4258,19 @@ var Task = class {
|
|
|
4021
4258
|
}
|
|
4022
4259
|
return tasksConfig.git.default_branch;
|
|
4023
4260
|
}
|
|
4261
|
+
get projectId() {
|
|
4262
|
+
return this.ctx.projectId;
|
|
4263
|
+
}
|
|
4024
4264
|
constructor(ctx) {
|
|
4025
4265
|
this.id = ctx.taskId;
|
|
4026
4266
|
this.ctx = ctx;
|
|
4027
4267
|
this.mcpServer = createTasksSdkServer(this);
|
|
4268
|
+
const timeoutMinutes = parseInt(process.env.JIVE_TASK_IDLE_TIMEOUT ?? "5", 10);
|
|
4269
|
+
this.idleTimeoutMs = isNaN(timeoutMinutes) ? 300 * 1e3 : timeoutMinutes * 60 * 1e3;
|
|
4270
|
+
this.debugLog(`Idle timeout configured: ${this.idleTimeoutMs / 6e4} minutes`);
|
|
4028
4271
|
}
|
|
4029
4272
|
debugLog(message) {
|
|
4030
|
-
|
|
4031
|
-
type: "debug_log",
|
|
4032
|
-
message
|
|
4033
|
-
}).replace(/\n/g, "\\n"));
|
|
4273
|
+
debugLog(`[Task ${this.id}] ${message}`);
|
|
4034
4274
|
}
|
|
4035
4275
|
startHeartbeat() {
|
|
4036
4276
|
this.heartbeatInterval = setInterval(() => {
|
|
@@ -4043,6 +4283,59 @@ var Task = class {
|
|
|
4043
4283
|
this.heartbeatInterval = null;
|
|
4044
4284
|
}
|
|
4045
4285
|
}
|
|
4286
|
+
updateLastActivity() {
|
|
4287
|
+
this.lastActivityTime = Date.now();
|
|
4288
|
+
this.debugLog(`Activity recorded at ${new Date(this.lastActivityTime).toISOString()}`);
|
|
4289
|
+
}
|
|
4290
|
+
startIdleWatchdog() {
|
|
4291
|
+
this.idleWatchdogInterval = setInterval(() => {
|
|
4292
|
+
this.checkIdleTimeout();
|
|
4293
|
+
}, 3e4);
|
|
4294
|
+
this.debugLog("Idle watchdog started");
|
|
4295
|
+
}
|
|
4296
|
+
stopIdleWatchdog() {
|
|
4297
|
+
if (this.idleWatchdogInterval) {
|
|
4298
|
+
clearInterval(this.idleWatchdogInterval);
|
|
4299
|
+
this.idleWatchdogInterval = null;
|
|
4300
|
+
this.debugLog("Idle watchdog stopped");
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
checkIdleTimeout() {
|
|
4304
|
+
if (this.idleTimeoutDisabled) {
|
|
4305
|
+
this.debugLog("Idle timeout is disabled, skipping check");
|
|
4306
|
+
return;
|
|
4307
|
+
}
|
|
4308
|
+
const idleDuration = Date.now() - this.lastActivityTime;
|
|
4309
|
+
const idleMinutes = Math.floor(idleDuration / 6e4);
|
|
4310
|
+
const idleSeconds = Math.floor(idleDuration % 6e4 / 1e3);
|
|
4311
|
+
this.debugLog(`Idle check: ${idleMinutes}m ${idleSeconds}s (threshold: ${this.idleTimeoutMs / 6e4}m)`);
|
|
4312
|
+
if (idleDuration >= this.idleTimeoutMs) {
|
|
4313
|
+
this.debugLog(`Idle timeout reached (${idleDuration}ms >= ${this.idleTimeoutMs}ms)`);
|
|
4314
|
+
this.handleIdleTimeout();
|
|
4315
|
+
}
|
|
4316
|
+
}
|
|
4317
|
+
async handleIdleTimeout() {
|
|
4318
|
+
this.debugLog("Task idle timeout - initiating graceful shutdown");
|
|
4319
|
+
this.stopIdleWatchdog();
|
|
4320
|
+
this.sendToTaskRunner({
|
|
4321
|
+
type: "process_status_changed",
|
|
4322
|
+
payload: {
|
|
4323
|
+
status: "exited",
|
|
4324
|
+
reason: "idle_timeout",
|
|
4325
|
+
idleDuration: Date.now() - this.lastActivityTime
|
|
4326
|
+
}
|
|
4327
|
+
});
|
|
4328
|
+
if (this.claudeAbortController) {
|
|
4329
|
+
this.debugLog("Aborting ongoing Claude query due to timeout");
|
|
4330
|
+
this.claudeAbortController.abort();
|
|
4331
|
+
this.claudeAbortController = null;
|
|
4332
|
+
}
|
|
4333
|
+
this.stopHeartbeat();
|
|
4334
|
+
this.stopSessionFileWatcher();
|
|
4335
|
+
this.cleanupCloudflareTunnel();
|
|
4336
|
+
this.debugLog("Task process exiting due to idle timeout");
|
|
4337
|
+
process.exit(0);
|
|
4338
|
+
}
|
|
4046
4339
|
async start() {
|
|
4047
4340
|
this.listenForMessages();
|
|
4048
4341
|
this.sendStatusUpdate("starting");
|
|
@@ -4057,12 +4350,14 @@ var Task = class {
|
|
|
4057
4350
|
}
|
|
4058
4351
|
this.debugLog(`✓ Git repository and session file setup complete`);
|
|
4059
4352
|
this.startHeartbeat();
|
|
4353
|
+
this.startIdleWatchdog();
|
|
4060
4354
|
this.sendStatusUpdate("idle");
|
|
4061
4355
|
this.listenForProcessExit();
|
|
4062
4356
|
}
|
|
4063
4357
|
listenForProcessExit() {
|
|
4064
4358
|
const cleanup = () => {
|
|
4065
4359
|
this.stopHeartbeat();
|
|
4360
|
+
this.stopIdleWatchdog();
|
|
4066
4361
|
this.stopSessionFileWatcher();
|
|
4067
4362
|
this.cleanupCloudflareTunnel();
|
|
4068
4363
|
this.sendStatusUpdate("exited");
|
|
@@ -4199,6 +4494,7 @@ var Task = class {
|
|
|
4199
4494
|
this.debugLog(`Received permission response for unknown request: ${payload.requestId}`);
|
|
4200
4495
|
return;
|
|
4201
4496
|
}
|
|
4497
|
+
this.updateLastActivity();
|
|
4202
4498
|
clearTimeout(pending.timeout);
|
|
4203
4499
|
pending.resolve(payload.approved);
|
|
4204
4500
|
this.pendingPermissions.delete(payload.requestId);
|
|
@@ -4212,11 +4508,13 @@ var Task = class {
|
|
|
4212
4508
|
this.debugLog(`Received question response for unknown request: ${payload.requestId}`);
|
|
4213
4509
|
return;
|
|
4214
4510
|
}
|
|
4511
|
+
this.updateLastActivity();
|
|
4215
4512
|
clearTimeout(pending.timeout);
|
|
4216
4513
|
pending.resolve(payload.answers);
|
|
4217
4514
|
this.pendingQuestions.delete(payload.requestId);
|
|
4218
4515
|
}
|
|
4219
4516
|
processMessage(inputMessage) {
|
|
4517
|
+
this.updateLastActivity();
|
|
4220
4518
|
switch (inputMessage.type) {
|
|
4221
4519
|
case "stop":
|
|
4222
4520
|
this.handleStop();
|
|
@@ -4264,6 +4562,14 @@ var Task = class {
|
|
|
4264
4562
|
});
|
|
4265
4563
|
}
|
|
4266
4564
|
/**
|
|
4565
|
+
* Disable the idle timeout for this task.
|
|
4566
|
+
* Useful when the task is serving an application and should not timeout while waiting for user testing.
|
|
4567
|
+
*/
|
|
4568
|
+
disableIdleTimeout() {
|
|
4569
|
+
this.idleTimeoutDisabled = true;
|
|
4570
|
+
this.debugLog("Idle timeout disabled");
|
|
4571
|
+
}
|
|
4572
|
+
/**
|
|
4267
4573
|
* Set up Cloudflare tunnel for exposing local services to the internet.
|
|
4268
4574
|
* Sets HOST and PORT environment variables for use by the task.
|
|
4269
4575
|
*/
|
|
@@ -4309,12 +4615,12 @@ var Task = class {
|
|
|
4309
4615
|
}
|
|
4310
4616
|
convertToHttpsUrl(repository, gitAuth) {
|
|
4311
4617
|
if (repository.startsWith("https://")) return repository;
|
|
4312
|
-
if (gitAuth.provider === "
|
|
4618
|
+
if (gitAuth.provider === "GITHUB" && repository.includes("git@github.com:")) {
|
|
4313
4619
|
const httpsUrl = `https://github.com/${repository.replace("git@github.com:", "")}`;
|
|
4314
4620
|
this.debugLog(`Converting SSH URL to HTTPS: ${repository} -> ${httpsUrl}`);
|
|
4315
4621
|
return httpsUrl;
|
|
4316
4622
|
}
|
|
4317
|
-
if (gitAuth.provider === "
|
|
4623
|
+
if (gitAuth.provider === "GITLAB" && repository.includes("git@gitlab.com:")) {
|
|
4318
4624
|
const httpsUrl = `https://gitlab.com/${repository.replace("git@gitlab.com:", "")}`;
|
|
4319
4625
|
this.debugLog(`Converting SSH URL to HTTPS: ${repository} -> ${httpsUrl}`);
|
|
4320
4626
|
return httpsUrl;
|
|
@@ -4361,9 +4667,6 @@ Host gitlab.com
|
|
|
4361
4667
|
this.debugLog("✓ SSH authentication configured with deploy key");
|
|
4362
4668
|
}
|
|
4363
4669
|
/**
|
|
4364
|
-
* Clean up SSH key files and environment variables
|
|
4365
|
-
*/
|
|
4366
|
-
/**
|
|
4367
4670
|
* Stop the Cloudflare tunnel and clean up resources
|
|
4368
4671
|
*/
|
|
4369
4672
|
cleanupCloudflareTunnel() {
|
|
@@ -4382,8 +4685,8 @@ Host gitlab.com
|
|
|
4382
4685
|
const { gitAuth } = this.ctx;
|
|
4383
4686
|
await execAsync$1("git init");
|
|
4384
4687
|
await execAsync$1("git config user.name \"Claude Agent | Jive\"");
|
|
4385
|
-
await execAsync$1("git config user.email \"claude@
|
|
4386
|
-
if (gitAuth) if (gitAuth.method === "
|
|
4688
|
+
await execAsync$1("git config user.email \"claude@getjive.app\"");
|
|
4689
|
+
if (gitAuth) if (gitAuth.method === "SSH") {
|
|
4387
4690
|
await this.setupSshAuthentication(gitAuth);
|
|
4388
4691
|
if (gitAuth.repositoryUrl) {
|
|
4389
4692
|
repository = gitAuth.repositoryUrl;
|
|
@@ -4444,7 +4747,7 @@ Host gitlab.com
|
|
|
4444
4747
|
sentLineIds = /* @__PURE__ */ new Set();
|
|
4445
4748
|
async ensureSessionFile() {
|
|
4446
4749
|
if (!await exists(this.claudeSessionFilePath)) await mkdir(path$1.dirname(this.claudeSessionFilePath), { recursive: true });
|
|
4447
|
-
const messages = await getApiClient().getSessionLines(this.ctx.taskId, this.ctx.sessionId);
|
|
4750
|
+
const messages = await getApiClient().getSessionLines(this.ctx.taskId, this.ctx.sessionId, true);
|
|
4448
4751
|
const sessionLines = [...messages.length === 0 ? makeInitialLines(this.ctx.claudeSessionId) : [], ...messages.map((m) => m.payload)];
|
|
4449
4752
|
this.sentLineIds = new Set(sessionLines.map((l$1) => l$1.uuid ?? null));
|
|
4450
4753
|
const jsonlContent = sessionLines.map((m) => JSON.stringify(m)).join("\n");
|
|
@@ -4486,18 +4789,22 @@ Host gitlab.com
|
|
|
4486
4789
|
}
|
|
4487
4790
|
});
|
|
4488
4791
|
this.fileWatcher.on("change", async () => {
|
|
4792
|
+
this.updateLastActivity();
|
|
4489
4793
|
const newLines = await this.readNewSessionLines();
|
|
4490
4794
|
for (const line of newLines) try {
|
|
4491
4795
|
const parsed = JSON.parse(line);
|
|
4492
4796
|
const uuid = parsed.uuid ?? null;
|
|
4493
4797
|
if (this.sentLineIds.has(uuid)) continue;
|
|
4494
4798
|
this.sentLineIds.add(uuid);
|
|
4799
|
+
const pendingLineUuid = parsed.type === "user" && this.currentPendingLineUuid ? this.currentPendingLineUuid : void 0;
|
|
4800
|
+
if (pendingLineUuid) this.currentPendingLineUuid = null;
|
|
4495
4801
|
this.sendToTaskRunner({
|
|
4496
4802
|
type: "session_changed",
|
|
4497
4803
|
payload: {
|
|
4498
4804
|
sessionId: this.ctx.sessionId,
|
|
4499
4805
|
line: parsed,
|
|
4500
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4806
|
+
timestamp: parsed.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
4807
|
+
pendingLineUuid
|
|
4501
4808
|
}
|
|
4502
4809
|
});
|
|
4503
4810
|
} catch (parseError) {
|
|
@@ -4517,6 +4824,7 @@ Host gitlab.com
|
|
|
4517
4824
|
this.lastFilePosition = 0;
|
|
4518
4825
|
}
|
|
4519
4826
|
async handleUserMessage(payload) {
|
|
4827
|
+
if (payload.pendingLineUuid) this.currentPendingLineUuid = payload.pendingLineUuid;
|
|
4520
4828
|
await this.queryClaude(payload.content, this.ctx.permissionMode);
|
|
4521
4829
|
}
|
|
4522
4830
|
claudeAbortController = null;
|
|
@@ -4525,11 +4833,12 @@ Host gitlab.com
|
|
|
4525
4833
|
this.claudeAbortController = null;
|
|
4526
4834
|
this.sendStatusUpdate("idle");
|
|
4527
4835
|
}
|
|
4528
|
-
async queryClaude(prompt, mode = "
|
|
4836
|
+
async queryClaude(prompt, mode = "BYPASS_PERMISSIONS") {
|
|
4529
4837
|
if (this.status !== "idle") {
|
|
4530
4838
|
this.debugLog(`WARNING: queryClaude called while status is '${this.status}'`);
|
|
4531
4839
|
return;
|
|
4532
4840
|
}
|
|
4841
|
+
this.updateLastActivity();
|
|
4533
4842
|
this.sendStatusUpdate("working");
|
|
4534
4843
|
this.claudeAbortController = new AbortController();
|
|
4535
4844
|
await this.startSessionFileWatcher();
|
|
@@ -4702,18 +5011,43 @@ async function setupRunnerCommand(options) {
|
|
|
4702
5011
|
}
|
|
4703
5012
|
await requireAuth();
|
|
4704
5013
|
try {
|
|
4705
|
-
const answers = await prompts([
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
5014
|
+
const answers = await prompts([
|
|
5015
|
+
{
|
|
5016
|
+
type: options.name ? null : "text",
|
|
5017
|
+
name: "name",
|
|
5018
|
+
message: "Runner name:",
|
|
5019
|
+
initial: `runner-${Date.now()}`,
|
|
5020
|
+
validate: (value$1) => value$1.length > 0 || "Name is required"
|
|
5021
|
+
},
|
|
5022
|
+
{
|
|
5023
|
+
type: "password",
|
|
5024
|
+
name: "anthropicApiKey",
|
|
5025
|
+
message: "Anthropic API key (https://console.anthropic.com/settings/keys):",
|
|
5026
|
+
validate: (value$1) => value$1.startsWith("sk-ant-") || "Invalid API key format"
|
|
5027
|
+
},
|
|
5028
|
+
{
|
|
5029
|
+
type: "number",
|
|
5030
|
+
name: "dockerCpus",
|
|
5031
|
+
message: "Docker CPU cores:",
|
|
5032
|
+
initial: 2,
|
|
5033
|
+
min: 1
|
|
5034
|
+
},
|
|
5035
|
+
{
|
|
5036
|
+
type: "text",
|
|
5037
|
+
name: "dockerMemory",
|
|
5038
|
+
message: "Docker memory limit (e.g., 2g, 4g, 512m):",
|
|
5039
|
+
initial: "2g",
|
|
5040
|
+
validate: (val) => /^\d+[kmg]$/i.test(val) || "Format: number + unit (k/m/g)"
|
|
5041
|
+
},
|
|
5042
|
+
{
|
|
5043
|
+
type: "number",
|
|
5044
|
+
name: "maxConcurrentTasks",
|
|
5045
|
+
message: "Maximum concurrent tasks:",
|
|
5046
|
+
initial: 5,
|
|
5047
|
+
min: 1,
|
|
5048
|
+
max: 50
|
|
5049
|
+
}
|
|
5050
|
+
]);
|
|
4717
5051
|
if (!answers.name && !options.name) {
|
|
4718
5052
|
console.log(chalk.yellow("Setup cancelled"));
|
|
4719
5053
|
return;
|
|
@@ -4736,7 +5070,14 @@ async function setupRunnerCommand(options) {
|
|
|
4736
5070
|
name: runnerName,
|
|
4737
5071
|
type: runnerType,
|
|
4738
5072
|
teamId: team$1.id.toString(),
|
|
4739
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5073
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5074
|
+
resourceLimits: {
|
|
5075
|
+
maxConcurrentTasks: answers.maxConcurrentTasks,
|
|
5076
|
+
docker: {
|
|
5077
|
+
cpus: answers.dockerCpus.toString(),
|
|
5078
|
+
memory: answers.dockerMemory
|
|
5079
|
+
}
|
|
5080
|
+
}
|
|
4740
5081
|
});
|
|
4741
5082
|
await updateCredentials({ anthropicApiKey });
|
|
4742
5083
|
spinner.succeed(`Runner '${runnerName}' configured!`);
|
|
@@ -5106,7 +5447,7 @@ const taskCommands = { resume: resumeCommand };
|
|
|
5106
5447
|
|
|
5107
5448
|
//#endregion
|
|
5108
5449
|
//#region package.json
|
|
5109
|
-
var version = "0.0.
|
|
5450
|
+
var version = "0.0.32";
|
|
5110
5451
|
|
|
5111
5452
|
//#endregion
|
|
5112
5453
|
//#region src/index.ts
|
|
@@ -5131,4 +5472,4 @@ program.command("task").description("Manage tasks").command("resume").descriptio
|
|
|
5131
5472
|
program.parse();
|
|
5132
5473
|
|
|
5133
5474
|
//#endregion
|
|
5134
|
-
export {
|
|
5475
|
+
export { queries as a, mutations as i, cliMutations as n, cliQueries as r, getGraphQLClient as t };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"private": false,
|
|
3
3
|
"name": "@jive-ai/cli",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.32",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
7
7
|
"dist",
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
16
16
|
"typecheck": "tsc --noEmit",
|
|
17
17
|
"build": "tsdown && npm pack && npm install -g jive-ai-cli-*.tgz",
|
|
18
|
-
"docker:build": "touch jive-ai-cli-0.0.0.tgz && docker build -t jive-task
|
|
19
|
-
"docker:build:local": "bun run build && docker build --build-arg USE_LOCAL=true -t jive-task
|
|
18
|
+
"docker:build": "touch jive-ai-cli-0.0.0.tgz && docker build -t jive-ai/task:latest . && rm -f jive-ai-cli-*.tgz",
|
|
19
|
+
"docker:build:local": "bun run build && docker build --build-arg USE_LOCAL=true -t jive-ai/task:latest .",
|
|
20
|
+
"docker:build:public": "touch jive-ai-cli-0.0.0.tgz && docker buildx build --platform linux/amd64,linux/arm64 -t jive-ai/task:latest -t jive-ai/task:$npm_package_version . && rm -f jive-ai-cli-*.tgz",
|
|
21
|
+
"docker:push": "docker buildx build --platform linux/amd64,linux/arm64 --push -t jive-ai/task:latest -t jive-ai/task:$npm_package_version .",
|
|
22
|
+
"docker:publish": "touch jive-ai-cli-0.0.0.tgz && docker buildx build --platform linux/amd64,linux/arm64 --push -t jive-ai/task:latest -t jive-ai/task:$npm_package_version . && rm -f jive-ai-cli-*.tgz",
|
|
20
23
|
"prepublishOnly": "npm run typecheck && npm run build"
|
|
21
24
|
},
|
|
22
25
|
"author": "",
|
|
@@ -31,7 +34,7 @@
|
|
|
31
34
|
"tsx": "^4.20.6"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
34
|
-
"@anthropic-ai/claude-agent-sdk": "^0.
|
|
37
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.7",
|
|
35
38
|
"@modelcontextprotocol/sdk": "^1.22.0",
|
|
36
39
|
"chalk": "^5.6.2",
|
|
37
40
|
"change-case": "^5.4.4",
|
|
@@ -50,6 +53,6 @@
|
|
|
50
53
|
"typescript": "^5.9.3",
|
|
51
54
|
"uuid": "^13.0.0",
|
|
52
55
|
"ws": "^8.18.0",
|
|
53
|
-
"zod": "^3.
|
|
56
|
+
"zod": "^4.3.5"
|
|
54
57
|
}
|
|
55
58
|
}
|