@heyhuynhgiabuu/pi-task 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/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/agents/explore.md +61 -0
- package/agents/planner.md +65 -0
- package/agents/reviewer.md +69 -0
- package/agents/scout.md +67 -0
- package/agents/vision.md +65 -0
- package/agents/worker.md +60 -0
- package/dist/agent-tools.d.ts +38 -0
- package/dist/agent-tools.js +91 -0
- package/dist/helpers.d.ts +99 -0
- package/dist/helpers.js +433 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +969 -0
- package/dist/policy.d.ts +3 -0
- package/dist/policy.js +35 -0
- package/dist/session-text.d.ts +7 -0
- package/dist/session-text.js +50 -0
- package/dist/subagent/buildArgv.d.ts +13 -0
- package/dist/subagent/buildArgv.js +25 -0
- package/dist/subagent/runSdk.d.ts +17 -0
- package/dist/subagent/runSdk.js +63 -0
- package/dist/subagent/tmux.d.ts +14 -0
- package/dist/subagent/tmux.js +72 -0
- package/dist/subagent/waitCompletion.d.ts +17 -0
- package/dist/subagent/waitCompletion.js +66 -0
- package/media/demo.png +0 -0
- package/package.json +63 -0
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Minimal shared tool-policy helpers for task agent frontmatter. */
|
|
2
|
+
export declare const XAI_SIDE_TOOL_NAMES: readonly ["xai_generate_text", "xai_multi_agent", "xai_web_search", "xai_x_search", "xai_code_execution", "xai_generate_image", "xai_analyze_image", "xai_deep_research", "xai_critique"];
|
|
3
|
+
export declare function parseMergedDisallowedTools(raw?: string): string[];
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Minimal shared tool-policy helpers for task agent frontmatter. */
|
|
2
|
+
export const XAI_SIDE_TOOL_NAMES = [
|
|
3
|
+
"xai_generate_text",
|
|
4
|
+
"xai_multi_agent",
|
|
5
|
+
"xai_web_search",
|
|
6
|
+
"xai_x_search",
|
|
7
|
+
"xai_code_execution",
|
|
8
|
+
"xai_generate_image",
|
|
9
|
+
"xai_analyze_image",
|
|
10
|
+
"xai_deep_research",
|
|
11
|
+
"xai_critique",
|
|
12
|
+
];
|
|
13
|
+
function xaiSideToolsEnabled() {
|
|
14
|
+
const raw = (process.env.PI_XAI_SIDE_TOOLS ?? "").trim().toLowerCase();
|
|
15
|
+
return raw === "1" || raw === "true" || raw === "yes";
|
|
16
|
+
}
|
|
17
|
+
export function parseMergedDisallowedTools(raw) {
|
|
18
|
+
const result = new Set();
|
|
19
|
+
if (!xaiSideToolsEnabled()) {
|
|
20
|
+
for (const tool of XAI_SIDE_TOOL_NAMES)
|
|
21
|
+
result.add(tool);
|
|
22
|
+
}
|
|
23
|
+
for (const token of (raw ?? "").split(",")) {
|
|
24
|
+
const value = token.trim();
|
|
25
|
+
if (!value)
|
|
26
|
+
continue;
|
|
27
|
+
if (value === "xai") {
|
|
28
|
+
for (const tool of XAI_SIDE_TOOL_NAMES)
|
|
29
|
+
result.add(tool);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
result.add(value);
|
|
33
|
+
}
|
|
34
|
+
return [...result];
|
|
35
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read assistant text from pi JSONL session directories (task / harness).
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
function extractText(content) {
|
|
7
|
+
if (typeof content === "string")
|
|
8
|
+
return content;
|
|
9
|
+
if (!Array.isArray(content))
|
|
10
|
+
return "";
|
|
11
|
+
return content
|
|
12
|
+
.filter((b) => b?.type === "text")
|
|
13
|
+
.map((b) => b.text ?? "")
|
|
14
|
+
.join("\n")
|
|
15
|
+
.trim();
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Last non-empty assistant message across all .jsonl files in sessionDir.
|
|
19
|
+
*/
|
|
20
|
+
export function getLastAssistantTextFromSessionDir(sessionDir) {
|
|
21
|
+
if (!existsSync(sessionDir))
|
|
22
|
+
return "";
|
|
23
|
+
const files = readdirSync(sessionDir)
|
|
24
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
25
|
+
.sort();
|
|
26
|
+
let last = "";
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const content = readFileSync(join(sessionDir, file), "utf-8");
|
|
29
|
+
for (const rawLine of content.split("\n")) {
|
|
30
|
+
const line = rawLine.trim();
|
|
31
|
+
if (!line)
|
|
32
|
+
continue;
|
|
33
|
+
try {
|
|
34
|
+
const entry = JSON.parse(line);
|
|
35
|
+
if (entry.type !== "message")
|
|
36
|
+
continue;
|
|
37
|
+
const msg = entry.message;
|
|
38
|
+
if (!msg || msg.role !== "assistant")
|
|
39
|
+
continue;
|
|
40
|
+
const text = extractText(msg.content);
|
|
41
|
+
if (text)
|
|
42
|
+
last = text;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* skip malformed JSONL rows */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return last;
|
|
50
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build `pi` CLI argv for subagent spawns.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentConfig } from "../helpers.js";
|
|
5
|
+
export interface BuildPiArgvOptions {
|
|
6
|
+
agent: AgentConfig;
|
|
7
|
+
sessionName: string;
|
|
8
|
+
sessionDir: string;
|
|
9
|
+
promptContent: string;
|
|
10
|
+
resume?: boolean;
|
|
11
|
+
parentToolNames?: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function buildPiArgv(opts: BuildPiArgvOptions): string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build `pi` CLI argv for subagent spawns.
|
|
3
|
+
*/
|
|
4
|
+
import { resolveAgentToolAllowlist } from "../agent-tools.js";
|
|
5
|
+
export function buildPiArgv(opts) {
|
|
6
|
+
const { agent, sessionName, sessionDir, promptContent, resume } = opts;
|
|
7
|
+
const allowedTools = resolveAgentToolAllowlist({
|
|
8
|
+
tools: agent.tools,
|
|
9
|
+
disallowedTools: agent.disallowedTools,
|
|
10
|
+
parentToolNames: opts.parentToolNames,
|
|
11
|
+
});
|
|
12
|
+
const args = [];
|
|
13
|
+
if (agent.model)
|
|
14
|
+
args.push("--model", agent.model);
|
|
15
|
+
if (agent.thinking)
|
|
16
|
+
args.push("--thinking", agent.thinking);
|
|
17
|
+
args.push("--tools", allowedTools.join(","));
|
|
18
|
+
args.push("--name", sessionName);
|
|
19
|
+
args.push("--session-dir", sessionDir);
|
|
20
|
+
if (resume)
|
|
21
|
+
args.push("--session", sessionName);
|
|
22
|
+
args.push("--append-system-prompt", agent.body);
|
|
23
|
+
args.push(promptContent);
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { AgentConfig } from "../helpers.js";
|
|
3
|
+
export interface RunSdkSubagentOptions {
|
|
4
|
+
prompt: string;
|
|
5
|
+
agent: AgentConfig;
|
|
6
|
+
cwd: string;
|
|
7
|
+
ctx: ExtensionContext;
|
|
8
|
+
model?: string;
|
|
9
|
+
thinkingLevel?: string;
|
|
10
|
+
tools?: string[];
|
|
11
|
+
excludeTools?: string[];
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function runSdkSubagent(options: RunSdkSubagentOptions): Promise<{
|
|
15
|
+
output: string;
|
|
16
|
+
sessionPath?: string;
|
|
17
|
+
}>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
function resolveModel(ctx, requested) {
|
|
2
|
+
const registry = ctx.modelRegistry;
|
|
3
|
+
const available = registry?.getAll?.() ?? registry?.getAvailable?.() ?? [];
|
|
4
|
+
if (requested) {
|
|
5
|
+
const [provider, ...rest] = requested.split("/");
|
|
6
|
+
const modelId = rest.join("/");
|
|
7
|
+
const byProvider = available.find((model) => {
|
|
8
|
+
return model?.provider?.id === provider && model?.id === modelId;
|
|
9
|
+
});
|
|
10
|
+
if (byProvider)
|
|
11
|
+
return byProvider;
|
|
12
|
+
const byId = available.find((model) => model?.id === requested || model?.name === requested);
|
|
13
|
+
if (byId)
|
|
14
|
+
return byId;
|
|
15
|
+
}
|
|
16
|
+
return available[0];
|
|
17
|
+
}
|
|
18
|
+
export async function runSdkSubagent(options) {
|
|
19
|
+
const model = resolveModel(options.ctx, options.model ?? options.agent.model);
|
|
20
|
+
if (!model) {
|
|
21
|
+
throw new Error("No model available for SDK subagent execution");
|
|
22
|
+
}
|
|
23
|
+
const { createAgentSession, DefaultResourceLoader } = await import("@earendil-works/pi-coding-agent");
|
|
24
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
25
|
+
cwd: options.cwd,
|
|
26
|
+
systemPromptOverride: options.systemPrompt,
|
|
27
|
+
});
|
|
28
|
+
const { session } = await createAgentSession({
|
|
29
|
+
cwd: options.cwd,
|
|
30
|
+
model,
|
|
31
|
+
thinkingLevel: options.thinkingLevel,
|
|
32
|
+
tools: options.tools,
|
|
33
|
+
excludeTools: options.excludeTools,
|
|
34
|
+
resourceLoader,
|
|
35
|
+
});
|
|
36
|
+
await session.prompt(options.prompt);
|
|
37
|
+
const sessionPath = session.sessionFile;
|
|
38
|
+
const output = getLastAssistantText(session.messages);
|
|
39
|
+
return { output: output.trim(), sessionPath };
|
|
40
|
+
}
|
|
41
|
+
function getLastAssistantText(messages) {
|
|
42
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
43
|
+
const message = messages[index];
|
|
44
|
+
if (message?.role !== "assistant")
|
|
45
|
+
continue;
|
|
46
|
+
const content = message.content;
|
|
47
|
+
if (typeof content === "string")
|
|
48
|
+
return content;
|
|
49
|
+
if (Array.isArray(content)) {
|
|
50
|
+
return content
|
|
51
|
+
.map((part) => {
|
|
52
|
+
if (typeof part === "string")
|
|
53
|
+
return part;
|
|
54
|
+
if (typeof part?.text === "string")
|
|
55
|
+
return part.text;
|
|
56
|
+
return "";
|
|
57
|
+
})
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.join("\n");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux helpers for subagent panes (shared by task extension).
|
|
3
|
+
*/
|
|
4
|
+
export declare function tmuxCmd(args: string[]): string;
|
|
5
|
+
export declare function hasTmux(): boolean;
|
|
6
|
+
export declare function paneExists(paneId: string): boolean;
|
|
7
|
+
export declare function getCurrentPaneId(): string | null;
|
|
8
|
+
export declare function splitWindowPane(cwd: string, command: string): {
|
|
9
|
+
paneId: string;
|
|
10
|
+
originalPane: string | null;
|
|
11
|
+
};
|
|
12
|
+
export declare function killAgentPane(paneId: string, originalPane: string | null): void;
|
|
13
|
+
/** Inject keys into a running subagent pane (steer / follow-up). */
|
|
14
|
+
export declare function tmuxSteerPane(paneId: string, message: string): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tmux helpers for subagent panes (shared by task extension).
|
|
3
|
+
*/
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
export function tmuxCmd(args) {
|
|
6
|
+
return execFileSync("tmux", args, {
|
|
7
|
+
encoding: "utf-8",
|
|
8
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9
|
+
}).trim();
|
|
10
|
+
}
|
|
11
|
+
export function hasTmux() {
|
|
12
|
+
try {
|
|
13
|
+
execFileSync("tmux", ["-V"], { stdio: "pipe" });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function paneExists(paneId) {
|
|
21
|
+
try {
|
|
22
|
+
const out = tmuxCmd(["list-panes", "-a", "-F", "#{pane_id}"]);
|
|
23
|
+
return out.split("\n").includes(paneId);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getCurrentPaneId() {
|
|
30
|
+
try {
|
|
31
|
+
return tmuxCmd(["display-message", "-p", "#{pane_id}"]);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function splitWindowPane(cwd, command) {
|
|
38
|
+
const originalPane = getCurrentPaneId();
|
|
39
|
+
const paneId = tmuxCmd([
|
|
40
|
+
"split-window",
|
|
41
|
+
"-h",
|
|
42
|
+
"-P",
|
|
43
|
+
"-F",
|
|
44
|
+
"#{pane_id}",
|
|
45
|
+
"-c",
|
|
46
|
+
cwd,
|
|
47
|
+
command,
|
|
48
|
+
]);
|
|
49
|
+
return { paneId, originalPane };
|
|
50
|
+
}
|
|
51
|
+
export function killAgentPane(paneId, originalPane) {
|
|
52
|
+
try {
|
|
53
|
+
tmuxCmd(["kill-pane", "-t", paneId]);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* already dead */
|
|
57
|
+
}
|
|
58
|
+
if (originalPane) {
|
|
59
|
+
try {
|
|
60
|
+
tmuxCmd(["select-pane", "-t", originalPane]);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* ignore */
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Inject keys into a running subagent pane (steer / follow-up). */
|
|
68
|
+
export function tmuxSteerPane(paneId, message) {
|
|
69
|
+
const escaped = message.replace(/'/g, `'\"'\"'`);
|
|
70
|
+
tmuxCmd(["send-keys", "-t", paneId, "-l", escaped]);
|
|
71
|
+
tmuxCmd(["send-keys", "-t", paneId, "Enter"]);
|
|
72
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type TaskCompletionStatus = "running" | "completed" | "failed" | "cancelled" | "timeout";
|
|
2
|
+
export interface TaskCompletionSnapshot {
|
|
3
|
+
status: TaskCompletionStatus;
|
|
4
|
+
content: string;
|
|
5
|
+
source?: "result-file" | "session-jsonl" | "pane" | "timeout" | "signal";
|
|
6
|
+
}
|
|
7
|
+
export interface TaskCompletionOptions {
|
|
8
|
+
resultPath: string;
|
|
9
|
+
sessionDir: string;
|
|
10
|
+
sessionName: string;
|
|
11
|
+
paneId?: string;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
timeoutMs?: number;
|
|
14
|
+
pollMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare function checkTaskCompletion(options: TaskCompletionOptions): Promise<TaskCompletionSnapshot>;
|
|
17
|
+
export declare function waitForTaskCompletion(options: TaskCompletionOptions): Promise<TaskCompletionSnapshot>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { getLastAssistantTextFromSessionDir } from "../session-text.js";
|
|
5
|
+
import { paneExists } from "./tmux.js";
|
|
6
|
+
async function readResultFile(resultPath) {
|
|
7
|
+
if (!existsSync(resultPath))
|
|
8
|
+
return null;
|
|
9
|
+
const text = (await readFile(resultPath, "utf-8")).trim();
|
|
10
|
+
return text.length > 0 ? text : null;
|
|
11
|
+
}
|
|
12
|
+
function readSessionText(sessionDir, sessionName) {
|
|
13
|
+
const sessionPath = join(sessionDir, "sessions", sessionName);
|
|
14
|
+
const text = getLastAssistantTextFromSessionDir(sessionPath).trim();
|
|
15
|
+
return text.length > 0 ? text : null;
|
|
16
|
+
}
|
|
17
|
+
export async function checkTaskCompletion(options) {
|
|
18
|
+
const result = await readResultFile(options.resultPath);
|
|
19
|
+
if (result) {
|
|
20
|
+
return { status: "completed", content: result, source: "result-file" };
|
|
21
|
+
}
|
|
22
|
+
if (options.paneId && paneExists(options.paneId)) {
|
|
23
|
+
return { status: "running", content: "", source: "pane" };
|
|
24
|
+
}
|
|
25
|
+
const sessionText = readSessionText(options.sessionDir, options.sessionName);
|
|
26
|
+
if (sessionText) {
|
|
27
|
+
return {
|
|
28
|
+
status: "completed",
|
|
29
|
+
content: sessionText,
|
|
30
|
+
source: "session-jsonl",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (options.paneId) {
|
|
34
|
+
return {
|
|
35
|
+
status: "failed",
|
|
36
|
+
content: "Task pane exited before producing a result or assistant response.",
|
|
37
|
+
source: "pane",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { status: "running", content: "", source: "pane" };
|
|
41
|
+
}
|
|
42
|
+
export async function waitForTaskCompletion(options) {
|
|
43
|
+
const timeoutMs = options.timeoutMs ?? 30 * 60 * 1000;
|
|
44
|
+
const pollMs = options.pollMs ?? 1000;
|
|
45
|
+
const deadline = Date.now() + timeoutMs;
|
|
46
|
+
while (true) {
|
|
47
|
+
if (options.signal?.aborted) {
|
|
48
|
+
return {
|
|
49
|
+
status: "cancelled",
|
|
50
|
+
content: "Task was cancelled.",
|
|
51
|
+
source: "signal",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const snapshot = await checkTaskCompletion(options);
|
|
55
|
+
if (snapshot.status !== "running")
|
|
56
|
+
return snapshot;
|
|
57
|
+
if (Date.now() >= deadline) {
|
|
58
|
+
return {
|
|
59
|
+
status: "timeout",
|
|
60
|
+
content: `Task timed out after ${Math.round(timeoutMs / 1000)}s without producing a result.`,
|
|
61
|
+
source: "timeout",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
65
|
+
}
|
|
66
|
+
}
|
package/media/demo.png
ADDED
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heyhuynhgiabuu/pi-task",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Delegating task/subagent extension for Pi: foreground/background subagents, widgets, tmux observability, SDK fallback.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"agents",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"media"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"typecheck": "tsc --noEmit",
|
|
25
|
+
"test": "PI_XAI_SIDE_TOOLS=0 tsx --test test/helpers.test.ts",
|
|
26
|
+
"smoke": "PI_XAI_SIDE_TOOLS=0 tsx test/smoke.ts",
|
|
27
|
+
"prepack": "npm run build"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"pi",
|
|
31
|
+
"pi-extension",
|
|
32
|
+
"agent",
|
|
33
|
+
"subagent",
|
|
34
|
+
"task",
|
|
35
|
+
"tmux"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@sinclair/typebox": "^0.34.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
47
|
+
"@types/node": "^20.0.0",
|
|
48
|
+
"tsx": "^4.20.6",
|
|
49
|
+
"typescript": "^5.0.0",
|
|
50
|
+
"@earendil-works/pi-tui": "*"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/heyhuynhgiabuu/pi-task.git"
|
|
58
|
+
},
|
|
59
|
+
"bugs": {
|
|
60
|
+
"url": "https://github.com/heyhuynhgiabuu/pi-task/issues"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://github.com/heyhuynhgiabuu/pi-task#readme"
|
|
63
|
+
}
|