@skillcdn/console 0.1.13 → 0.1.15
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 +5 -4
- package/SKILLCDN.md +2 -0
- package/dist/agent-name.d.ts +3 -0
- package/dist/agent-name.d.ts.map +1 -0
- package/dist/agent-name.js +9 -0
- package/dist/agent-name.js.map +1 -0
- package/dist/api.d.ts +6 -4
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +6 -4
- package/dist/api.js.map +1 -1
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.d.ts.map +1 -1
- package/dist/cli/cli.js +215 -22
- package/dist/cli/cli.js.map +1 -1
- package/dist/cli/format.d.ts +14 -4
- package/dist/cli/format.d.ts.map +1 -1
- package/dist/cli/format.js +59 -10
- package/dist/cli/format.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +27 -11
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/main.js +5 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/client.d.ts +20 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -2
- package/dist/client.js.map +1 -1
- package/dist/components/calendar.d.ts +15 -0
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +78 -41
- package/dist/components/calendar.js.map +1 -1
- package/dist/components/connect.d.ts +4 -1
- package/dist/components/connect.d.ts.map +1 -1
- package/dist/components/connect.js +7 -3
- package/dist/components/connect.js.map +1 -1
- package/dist/components/decision-list.d.ts.map +1 -1
- package/dist/components/decision-list.js +3 -3
- package/dist/components/decision-list.js.map +1 -1
- package/dist/components/documents.d.ts +5 -0
- package/dist/components/documents.d.ts.map +1 -1
- package/dist/components/documents.js +3 -2
- package/dist/components/documents.js.map +1 -1
- package/dist/components/event-feed.d.ts +1 -1
- package/dist/components/event-feed.d.ts.map +1 -1
- package/dist/components/event-feed.js +10 -1
- package/dist/components/event-feed.js.map +1 -1
- package/dist/components/people.d.ts +24 -1
- package/dist/components/people.d.ts.map +1 -1
- package/dist/components/people.js +35 -3
- package/dist/components/people.js.map +1 -1
- package/dist/components/projects.d.ts +6 -0
- package/dist/components/projects.d.ts.map +1 -1
- package/dist/components/projects.js +3 -3
- package/dist/components/projects.js.map +1 -1
- package/dist/components/rules.d.ts +40 -0
- package/dist/components/rules.d.ts.map +1 -0
- package/dist/components/rules.js +46 -0
- package/dist/components/rules.js.map +1 -0
- package/dist/components/runs.d.ts +1 -1
- package/dist/components/runs.d.ts.map +1 -1
- package/dist/components/runs.js +2 -1
- package/dist/components/runs.js.map +1 -1
- package/dist/components/skills.d.ts.map +1 -1
- package/dist/components/skills.js +5 -1
- package/dist/components/skills.js.map +1 -1
- package/dist/components/tokens.d.ts.map +1 -1
- package/dist/components/tokens.js +2 -9
- package/dist/components/tokens.js.map +1 -1
- package/dist/components/ui.d.ts +7 -2
- package/dist/components/ui.d.ts.map +1 -1
- package/dist/components/ui.js +13 -0
- package/dist/components/ui.js.map +1 -1
- package/dist/components/work.d.ts +9 -1
- package/dist/components/work.d.ts.map +1 -1
- package/dist/components/work.js +8 -3
- package/dist/components/work.js.map +1 -1
- package/dist/connect-page.d.ts.map +1 -1
- package/dist/connect-page.js +1 -1
- package/dist/connect-page.js.map +1 -1
- package/dist/console.css +253 -13
- package/dist/console.d.ts.map +1 -1
- package/dist/console.js +203 -49
- package/dist/console.js.map +1 -1
- package/dist/data.d.ts +19 -2
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -1
- package/dist/data.js.map +1 -1
- package/dist/dates.d.ts +4 -0
- package/dist/dates.d.ts.map +1 -1
- package/dist/dates.js +26 -0
- package/dist/dates.js.map +1 -1
- package/dist/i18n/en.d.ts +90 -28
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +89 -31
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/ko.d.ts.map +1 -1
- package/dist/i18n/ko.js +89 -31
- package/dist/i18n/ko.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +4 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -0
- package/dist/limits.js.map +1 -1
- package/dist/router.d.ts +34 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +55 -7
- package/dist/router.js.map +1 -1
- package/dist/routes.d.ts +14 -4
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +17 -2
- package/dist/routes.js.map +1 -1
- package/dist/rules.d.ts +28 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +102 -0
- package/dist/rules.js.map +1 -0
- package/dist/schemas.d.ts +652 -4
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +96 -6
- package/dist/schemas.js.map +1 -1
- package/dist/vocabulary.d.ts +21 -1
- package/dist/vocabulary.d.ts.map +1 -1
- package/dist/vocabulary.js +21 -0
- package/dist/vocabulary.js.map +1 -1
- package/docs/specs/cli.md +13 -9
- package/docs/specs/rest.md +32 -8
- package/package.json +1 -1
- package/skills/building-a-console/SKILL.md +1 -1
- package/skills/deploying-a-console/SKILL.md +5 -5
- package/skills/deploying-a-console/assets/secrets.example.json +2 -1
- package/skills/deploying-a-console/references/access.md +10 -6
- package/skills/deploying-a-console/references/resources.md +10 -7
- package/skills/working-the-board/SKILL.md +10 -9
- package/skills/working-the-board/references/project-rules.md +36 -0
- package/src/agent-name.ts +9 -0
- package/src/api.ts +37 -0
- package/src/cli/cli.ts +253 -24
- package/src/cli/format.ts +74 -10
- package/src/cli/help.ts +30 -10
- package/src/cli/main.ts +5 -0
- package/src/client.ts +47 -1
- package/src/components/calendar.tsx +242 -101
- package/src/components/connect.tsx +12 -6
- package/src/components/decision-list.tsx +2 -2
- package/src/components/documents.tsx +7 -3
- package/src/components/event-feed.tsx +16 -3
- package/src/components/people.tsx +244 -35
- package/src/components/projects.tsx +31 -8
- package/src/components/rules.tsx +167 -0
- package/src/components/runs.tsx +4 -2
- package/src/components/skills.tsx +22 -11
- package/src/components/tokens.tsx +2 -14
- package/src/components/ui.tsx +25 -2
- package/src/components/work.tsx +69 -43
- package/src/connect-page.tsx +0 -1
- package/src/console.tsx +531 -67
- package/src/data.ts +52 -2
- package/src/dates.ts +28 -0
- package/src/i18n/en.ts +99 -38
- package/src/i18n/ko.ts +95 -38
- package/src/index.ts +21 -1
- package/src/limits.ts +4 -0
- package/src/router.ts +77 -12
- package/src/routes.ts +23 -3
- package/src/rules.ts +109 -0
- package/src/schemas.ts +121 -4
- package/src/styles/console.css +253 -13
- package/src/vocabulary.ts +30 -0
package/src/cli/cli.ts
CHANGED
|
@@ -9,7 +9,14 @@ import {
|
|
|
9
9
|
} from "../client.js";
|
|
10
10
|
import { addDays, dayOf as dayOfDate, isDay } from "../dates.js";
|
|
11
11
|
import { DEFAULT_PAGE_LIMIT, LIST_LIMIT, MAX_OPTIONS, MIN_OPTIONS } from "../limits.js";
|
|
12
|
-
import
|
|
12
|
+
import { fillRules, RULES_PATH, RULES_TITLE } from "../rules.js";
|
|
13
|
+
import {
|
|
14
|
+
hostOfLogin,
|
|
15
|
+
type RestClaim,
|
|
16
|
+
type RestDecision,
|
|
17
|
+
type RestPerson,
|
|
18
|
+
type RestTaskPatch,
|
|
19
|
+
} from "../schemas.js";
|
|
13
20
|
import {
|
|
14
21
|
COLOR_NAMES,
|
|
15
22
|
type ColorName,
|
|
@@ -19,8 +26,10 @@ import {
|
|
|
19
26
|
type PersonRole,
|
|
20
27
|
PROJECT_ROLES,
|
|
21
28
|
PROJECT_VISIBILITIES,
|
|
29
|
+
PROVIDER_KEYS,
|
|
22
30
|
type ProjectRole,
|
|
23
31
|
type ProjectVisibility,
|
|
32
|
+
type ProviderKey,
|
|
24
33
|
TASK_PRIORITIES,
|
|
25
34
|
TASK_STATES,
|
|
26
35
|
type TaskPriority,
|
|
@@ -38,12 +47,15 @@ import {
|
|
|
38
47
|
formatDocuments,
|
|
39
48
|
formatGoal,
|
|
40
49
|
formatGoalLine,
|
|
50
|
+
formatListingLine,
|
|
41
51
|
formatMemberLine,
|
|
42
52
|
formatMore,
|
|
43
53
|
formatOptions,
|
|
44
54
|
formatPersonLine,
|
|
55
|
+
formatPersonWork,
|
|
45
56
|
formatProject,
|
|
46
57
|
formatProjectLine,
|
|
58
|
+
formatRules,
|
|
47
59
|
formatRun,
|
|
48
60
|
formatRunLine,
|
|
49
61
|
formatSkills,
|
|
@@ -102,6 +114,8 @@ export interface CliIo {
|
|
|
102
114
|
readonly now: () => number;
|
|
103
115
|
/** The version of the package the command runs from, when the entry point knows it. */
|
|
104
116
|
readonly version?: string | undefined;
|
|
117
|
+
/** Where the console's own skills are on this machine, shipped with the package; nothing when they are not there. */
|
|
118
|
+
readonly skillsDir?: string | undefined;
|
|
105
119
|
/** How `serve` listens on the loopback (ADR-0014); left out, the command cannot serve. */
|
|
106
120
|
readonly listen?: Listen | undefined;
|
|
107
121
|
}
|
|
@@ -368,6 +382,8 @@ function wordOf<T extends string>(value: string, words: readonly T[], what: stri
|
|
|
368
382
|
|
|
369
383
|
const personRoleOf = (value: string): PersonRole =>
|
|
370
384
|
wordOf(value, PERSON_ROLES, "a role in the workspace");
|
|
385
|
+
const providerOf = (value: string): ProviderKey =>
|
|
386
|
+
wordOf(value, PROVIDER_KEYS, "a provider people sign in through");
|
|
371
387
|
const projectRoleOf = (value: string): ProjectRole =>
|
|
372
388
|
wordOf(value, PROJECT_ROLES, "a role in a project");
|
|
373
389
|
const visibilityOf = (value: string): ProjectVisibility =>
|
|
@@ -522,7 +538,8 @@ const login: Command = async (args, io) => {
|
|
|
522
538
|
// A connection (ADR-0011): a code a person approves on the console's own pages, and the token
|
|
523
539
|
// handed over here once, never shown.
|
|
524
540
|
const client = createClient({ baseUrl: url, fetch: io.fetch });
|
|
525
|
-
|
|
541
|
+
// "<agent> on <machine>": the pages say the agent and keep the machine for a hover.
|
|
542
|
+
const agent = text(values, "agent") ?? agentOf(io.env) ?? "An agent";
|
|
526
543
|
const begun = await client.connect({
|
|
527
544
|
agent: io.hostname === undefined ? agent : `${agent} on ${io.hostname}`,
|
|
528
545
|
});
|
|
@@ -550,11 +567,47 @@ const login: Command = async (args, io) => {
|
|
|
550
567
|
|
|
551
568
|
const logout: Command = async (args, io) => {
|
|
552
569
|
parse(args, {}, false);
|
|
570
|
+
const stored = await io.store.load();
|
|
571
|
+
const url = io.env.CONSOLE_URL ?? stored?.url;
|
|
572
|
+
const token = io.env.CONSOLE_TOKEN ?? stored?.token;
|
|
573
|
+
if (url === undefined || token === undefined) {
|
|
574
|
+
io.stdout("Not signed in here: nothing to forget.\n");
|
|
575
|
+
return EXIT.ok;
|
|
576
|
+
}
|
|
577
|
+
// The token is given up at the console first, so that it is good for nothing from now on;
|
|
578
|
+
// one the console no longer knows is gone already. A console out of reach leaves it to the
|
|
579
|
+
// Agents page, and the person is told so.
|
|
580
|
+
let gone = true;
|
|
581
|
+
try {
|
|
582
|
+
await createClient({ baseUrl: url, token, fetch: io.fetch }).giveUpToken();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
if (!(error instanceof ApiError && error.code === "auth.required")) {
|
|
585
|
+
gone = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
553
588
|
await io.store.clear();
|
|
554
|
-
io.stdout(
|
|
589
|
+
io.stdout(
|
|
590
|
+
gone
|
|
591
|
+
? "Signed out: the token is given up at the console and forgotten here.\n"
|
|
592
|
+
: `Signed out here, but the console at ${url} could not be reached to give the token up: a person removes it on the Agents page.\n`,
|
|
593
|
+
);
|
|
594
|
+
if (io.env.CONSOLE_TOKEN !== undefined) {
|
|
595
|
+
io.stdout("CONSOLE_TOKEN in the environment still holds it: unset it.\n");
|
|
596
|
+
}
|
|
555
597
|
return EXIT.ok;
|
|
556
598
|
};
|
|
557
599
|
|
|
600
|
+
/** The agent the command runs under, where its environment says so: what a connection is named by default. */
|
|
601
|
+
function agentOf(env: CliIo["env"]): string | undefined {
|
|
602
|
+
if (env.CLAUDECODE !== undefined) {
|
|
603
|
+
return "Claude Code";
|
|
604
|
+
}
|
|
605
|
+
if (Object.keys(env).some((key) => key.startsWith("CODEX_"))) {
|
|
606
|
+
return "Codex";
|
|
607
|
+
}
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
|
|
558
611
|
const whoami: Command = async (args, io) => {
|
|
559
612
|
const { values } = parse(args, {}, false);
|
|
560
613
|
const session = await open(io, values);
|
|
@@ -597,7 +650,7 @@ const use: Command = async (args, io) => {
|
|
|
597
650
|
session,
|
|
598
651
|
project,
|
|
599
652
|
() =>
|
|
600
|
-
`Working in ${project.name} (${project.key}) from this directory on: kept in ${kept}, which is meant to be committed.`,
|
|
653
|
+
`Working in ${project.name} (${project.key}) from this directory on: kept in ${kept}, which is meant to be committed. What its people ask of agents: console rules.`,
|
|
601
654
|
);
|
|
602
655
|
return EXIT.ok;
|
|
603
656
|
};
|
|
@@ -653,10 +706,18 @@ const tasks: Command = async (args, io) => {
|
|
|
653
706
|
};
|
|
654
707
|
|
|
655
708
|
const work: Command = async (args, io) => {
|
|
656
|
-
const { values } = parse(args, {}, false);
|
|
709
|
+
const { values } = parse(args, { person: { type: "string" } }, false);
|
|
657
710
|
const session = await open(io, values);
|
|
658
|
-
const
|
|
659
|
-
|
|
711
|
+
const login = text(values, "person");
|
|
712
|
+
if (login === undefined) {
|
|
713
|
+
const found = await session.client.work();
|
|
714
|
+
answer(io, session, found, () => formatWork(found));
|
|
715
|
+
return EXIT.ok;
|
|
716
|
+
}
|
|
717
|
+
// Another person's plate: the console decides whether it is the asker's to see.
|
|
718
|
+
const whose = await personOf(session, login);
|
|
719
|
+
const found = await session.client.personWork(whose.id);
|
|
720
|
+
answer(io, session, found, () => formatPersonWork(whose, found));
|
|
660
721
|
return EXIT.ok;
|
|
661
722
|
};
|
|
662
723
|
|
|
@@ -675,10 +736,118 @@ const skills: Command = async (args, io) => {
|
|
|
675
736
|
const session = await open(io, values);
|
|
676
737
|
const project = await session.project();
|
|
677
738
|
const found = await project.skills();
|
|
678
|
-
answer(io, session, found, () => formatSkills(found));
|
|
739
|
+
answer(io, session, found, () => formatSkills(found, io.skillsDir));
|
|
740
|
+
return EXIT.ok;
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
/** A version number given as a flag, or nothing; anything else is not understood. */
|
|
744
|
+
function versionOf(given: string | undefined): number | undefined {
|
|
745
|
+
if (given === undefined) {
|
|
746
|
+
return undefined;
|
|
747
|
+
}
|
|
748
|
+
const version = Number(given);
|
|
749
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
750
|
+
throw misuse(`Not a version number: ${given}`);
|
|
751
|
+
}
|
|
752
|
+
return version;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// The rules of the board (ADR-0023): read before a task is taken, written by an owner for the
|
|
756
|
+
// project and by an administrator for the workspace, through the same page the console keeps.
|
|
757
|
+
const rules: Command = async (args, io) => {
|
|
758
|
+
const [verb, ...rest] = args;
|
|
759
|
+
if (verb === "write") {
|
|
760
|
+
const { values, positionals } = parse(rest, {
|
|
761
|
+
body: { type: "string" },
|
|
762
|
+
file: { type: "string" },
|
|
763
|
+
workspace: { type: "boolean" },
|
|
764
|
+
base: { type: "string" },
|
|
765
|
+
});
|
|
766
|
+
if (positionals.length > 1) {
|
|
767
|
+
throw misuse("Say the rules as one argument: console rules write --body <markdown>");
|
|
768
|
+
}
|
|
769
|
+
const given = positionals[0] ?? text(values, "body");
|
|
770
|
+
const none = given === "none";
|
|
771
|
+
const body = none ? undefined : await bodyOf(io, values, given);
|
|
772
|
+
if (!none && body === undefined) {
|
|
773
|
+
throw misuse(
|
|
774
|
+
"Say what the rules are to say: --body <markdown>, --file <path>, - to read standard input, or none to put the written rules away.",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
const baseVersion = versionOf(text(values, "base"));
|
|
778
|
+
const session = await open(io, values);
|
|
779
|
+
if (on(values, "workspace")) {
|
|
780
|
+
if (none) {
|
|
781
|
+
const put = await session.client.archiveRules();
|
|
782
|
+
answer(
|
|
783
|
+
io,
|
|
784
|
+
session,
|
|
785
|
+
put,
|
|
786
|
+
() => "The organization's rules are put away: the console's own hold.",
|
|
787
|
+
);
|
|
788
|
+
return EXIT.ok;
|
|
789
|
+
}
|
|
790
|
+
const written = await session.client.writeRules({
|
|
791
|
+
body: body ?? "",
|
|
792
|
+
...(baseVersion === undefined ? {} : { baseVersion }),
|
|
793
|
+
});
|
|
794
|
+
answer(io, session, written, () =>
|
|
795
|
+
written.page === null
|
|
796
|
+
? "Nothing written: these are the console's own rules as they stand, and they hold already."
|
|
797
|
+
: `Wrote the workspace's rules, version ${written.page.version}.`,
|
|
798
|
+
);
|
|
799
|
+
return EXIT.ok;
|
|
800
|
+
}
|
|
801
|
+
const project = await session.project();
|
|
802
|
+
if (none) {
|
|
803
|
+
try {
|
|
804
|
+
const put = await project.archiveDocument(RULES_PATH);
|
|
805
|
+
answer(io, session, put, () => "This project's rules are put away: it adds none now.");
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (!(error instanceof ApiError) || error.code !== "document.not_found") {
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
answer(io, session, null, () => "This project has no rules of its own to put away.");
|
|
811
|
+
}
|
|
812
|
+
return EXIT.ok;
|
|
813
|
+
}
|
|
814
|
+
const written = await project.writeDocument(RULES_PATH, {
|
|
815
|
+
title: RULES_TITLE,
|
|
816
|
+
body: body ?? "",
|
|
817
|
+
...(baseVersion === undefined ? {} : { baseVersion }),
|
|
818
|
+
});
|
|
819
|
+
answer(io, session, written, () => `Wrote this project's rules, version ${written.version}.`);
|
|
820
|
+
return EXIT.ok;
|
|
821
|
+
}
|
|
822
|
+
const { values } = parse(args, {}, false);
|
|
823
|
+
const session = await open(io, values);
|
|
824
|
+
const key = await session.projectKey();
|
|
825
|
+
const [found, me] = await Promise.all([
|
|
826
|
+
key === undefined ? session.client.rules() : session.client.project(key).rules(),
|
|
827
|
+
session.client.me(),
|
|
828
|
+
]);
|
|
829
|
+
const read =
|
|
830
|
+
"workspace" in found
|
|
831
|
+
? {
|
|
832
|
+
workspace: forPerson(found.workspace, me.language),
|
|
833
|
+
project: forPerson(found.project, me.language),
|
|
834
|
+
}
|
|
835
|
+
: forPerson(found, me.language);
|
|
836
|
+
answer(io, session, read, () => formatRules(read));
|
|
679
837
|
return EXIT.ok;
|
|
680
838
|
};
|
|
681
839
|
|
|
840
|
+
/**
|
|
841
|
+
* The rules as they read for the person whose token the command holds: `{language}` filled
|
|
842
|
+
* with the language they chose for the pages, or with the task's when they chose none.
|
|
843
|
+
*/
|
|
844
|
+
function forPerson<T extends { readonly body: string | null }>(
|
|
845
|
+
half: T,
|
|
846
|
+
language: string | null,
|
|
847
|
+
): T {
|
|
848
|
+
return half.body === null ? half : { ...half, body: fillRules(half.body, language) };
|
|
849
|
+
}
|
|
850
|
+
|
|
682
851
|
/** The flags that set a task's fields, shared by writing one and changing one. */
|
|
683
852
|
const TASK_FIELDS: OptionSpec = {
|
|
684
853
|
state: { type: "string" },
|
|
@@ -804,12 +973,21 @@ const take: Command = async (args, io) => {
|
|
|
804
973
|
taskId: found.id,
|
|
805
974
|
...(agent === undefined ? {} : { agent }),
|
|
806
975
|
});
|
|
807
|
-
|
|
976
|
+
// The rules come with the run, as they read for the person: everything from here acts
|
|
977
|
+
// within them (ADR-0023).
|
|
978
|
+
const [ruling, me] = await Promise.all([project.rules(), session.client.me()]);
|
|
979
|
+
const rules = {
|
|
980
|
+
workspace: forPerson(ruling.workspace, me.language),
|
|
981
|
+
project: forPerson(ruling.project, me.language),
|
|
982
|
+
};
|
|
983
|
+
answer(io, session, { run, task: found, rules }, () =>
|
|
808
984
|
[
|
|
809
985
|
`Run ${run.id} began on #${found.number} ${found.title}, as "${run.agent}" for ${run.person.login}. The task is in progress and theirs. A run that was open on it is over now: the board cannot know whether its agent still ran, so taking it was deciding.`,
|
|
810
986
|
"",
|
|
811
987
|
formatTask(found),
|
|
812
988
|
"",
|
|
989
|
+
formatRules(rules),
|
|
990
|
+
"",
|
|
813
991
|
'Say how it goes: console report "<what you found or did>". Hand in what you make: console hand-in <https url | file path>.',
|
|
814
992
|
'Ask when a person must decide: console ask "<question>" --option "..." --option "...". End with: console finish --summary "<what was done, what is left>".',
|
|
815
993
|
].join("\n"),
|
|
@@ -1403,25 +1581,67 @@ const people: Command = async (args, io) => {
|
|
|
1403
1581
|
const { values } = parse(args, {}, false);
|
|
1404
1582
|
const session = await open(io, values);
|
|
1405
1583
|
const found = await session.client.people();
|
|
1406
|
-
answer(io, session, found, () =>
|
|
1584
|
+
answer(io, session, found, () =>
|
|
1585
|
+
[...found.items.map(formatPersonLine), ...found.listed.map(formatListingLine)].join("\n"),
|
|
1586
|
+
);
|
|
1407
1587
|
return EXIT.ok;
|
|
1408
1588
|
};
|
|
1409
1589
|
|
|
1590
|
+
const PERSON_USAGE =
|
|
1591
|
+
"Say what to do: console person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>";
|
|
1592
|
+
|
|
1410
1593
|
const person: Command = async (args, io) => {
|
|
1411
|
-
|
|
1412
|
-
|
|
1594
|
+
const what = args[0];
|
|
1595
|
+
if (what === "role") {
|
|
1596
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
1597
|
+
const [login, role] = theTwo(
|
|
1598
|
+
positionals,
|
|
1599
|
+
"Say whom and what: console person role <login> <admin|member>",
|
|
1600
|
+
);
|
|
1601
|
+
const wanted = personRoleOf(role);
|
|
1602
|
+
const session = await open(io, values);
|
|
1603
|
+
const found = await personOf(session, login);
|
|
1604
|
+
const changed = await session.client.updatePerson(found.id, { role: wanted });
|
|
1605
|
+
answer(io, session, changed, () => formatPersonLine(changed));
|
|
1606
|
+
return EXIT.ok;
|
|
1413
1607
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
positionals,
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1608
|
+
if (what === "add") {
|
|
1609
|
+
const { values, positionals } = parse(args.slice(1), { host: { type: "string" } });
|
|
1610
|
+
const login = theOne(positionals, "Say whom: console person add <login> [--host gh|google]");
|
|
1611
|
+
const chosen = text(values, "host");
|
|
1612
|
+
const host = chosen === undefined ? hostOfLogin(login) : providerOf(chosen);
|
|
1613
|
+
const session = await open(io, values);
|
|
1614
|
+
const made = await session.client.createListing({ host, login });
|
|
1615
|
+
answer(io, session, made, () => formatListingLine(made));
|
|
1616
|
+
return EXIT.ok;
|
|
1617
|
+
}
|
|
1618
|
+
if (what === "remove") {
|
|
1619
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
1620
|
+
const login = theOne(positionals, "Say whom: console person remove <login>");
|
|
1621
|
+
const session = await open(io, values);
|
|
1622
|
+
const { listed } = await session.client.people();
|
|
1623
|
+
const found = listed.find((listing) => listing.login.toLowerCase() === login.toLowerCase());
|
|
1624
|
+
if (found === undefined) {
|
|
1625
|
+
throw misuse(
|
|
1626
|
+
`Nobody called ${login} is let in and not signed in yet. console people lists them; a person who signed in is taken out with console person deactivate <login>.`,
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
await session.client.removeListing(found.id);
|
|
1630
|
+
answer(io, session, { removed: found }, () => `${found.login} is no longer let in.`);
|
|
1631
|
+
return EXIT.ok;
|
|
1632
|
+
}
|
|
1633
|
+
if (what === "deactivate" || what === "reactivate") {
|
|
1634
|
+
const { values, positionals } = parse(args.slice(1), {});
|
|
1635
|
+
const login = theOne(positionals, `Say whom: console person ${what} <login>`);
|
|
1636
|
+
const session = await open(io, values);
|
|
1637
|
+
const found = await personOf(session, login);
|
|
1638
|
+
const changed = await session.client.updatePerson(found.id, {
|
|
1639
|
+
status: what === "deactivate" ? "deactivated" : "active",
|
|
1640
|
+
});
|
|
1641
|
+
answer(io, session, changed, () => formatPersonLine(changed));
|
|
1642
|
+
return EXIT.ok;
|
|
1643
|
+
}
|
|
1644
|
+
throw misuse(PERSON_USAGE);
|
|
1425
1645
|
};
|
|
1426
1646
|
|
|
1427
1647
|
/** The flags that set what a project is, shared by making one and changing one. */
|
|
@@ -1665,6 +1885,7 @@ const COMMANDS: Readonly<Record<string, Command>> = {
|
|
|
1665
1885
|
goal,
|
|
1666
1886
|
categories,
|
|
1667
1887
|
skills,
|
|
1888
|
+
rules,
|
|
1668
1889
|
people,
|
|
1669
1890
|
person,
|
|
1670
1891
|
project,
|
|
@@ -1743,7 +1964,15 @@ function hintFor(error: ApiError): string {
|
|
|
1743
1964
|
case "category.in_use":
|
|
1744
1965
|
return "Tasks are sorted into the category. Move them out first: console task edit <number> --category <other|none>\n";
|
|
1745
1966
|
case "person.last_admin":
|
|
1746
|
-
return "The board keeps at least one administrator. Make another one first.\n";
|
|
1967
|
+
return "The board keeps at least one administrator in. Make another one first.\n";
|
|
1968
|
+
case "person.of_organization":
|
|
1969
|
+
return "The organization says who is in: a person it let in comes and goes with it, and is listed by login to stay.\n";
|
|
1970
|
+
case "listing.exists":
|
|
1971
|
+
return "The login is let in already, or is a person's who is in. console people lists both.\n";
|
|
1972
|
+
case "listing.not_found":
|
|
1973
|
+
return "No login let in and not signed in yet has that id. console people lists them.\n";
|
|
1974
|
+
case "listing.unknown_host":
|
|
1975
|
+
return "Nobody signs in through that provider here. console whoami --json names the ways in.\n";
|
|
1747
1976
|
case "connect.too_many":
|
|
1748
1977
|
return "The console holds as many connections as it may at once. Try again in a few minutes.\n";
|
|
1749
1978
|
case "network":
|
package/src/cli/format.ts
CHANGED
|
@@ -6,9 +6,13 @@ import type {
|
|
|
6
6
|
RestDocumentSummary,
|
|
7
7
|
RestDocuments,
|
|
8
8
|
RestGoal,
|
|
9
|
+
RestListing,
|
|
9
10
|
RestMember,
|
|
10
11
|
RestPerson,
|
|
12
|
+
RestPersonWork,
|
|
11
13
|
RestProject,
|
|
14
|
+
RestProjectRules,
|
|
15
|
+
RestRules,
|
|
12
16
|
RestRun,
|
|
13
17
|
RestSkills,
|
|
14
18
|
RestTask,
|
|
@@ -57,11 +61,20 @@ export function formatProject(project: RestProject): string {
|
|
|
57
61
|
return lines.join("\n");
|
|
58
62
|
}
|
|
59
63
|
|
|
60
|
-
/** One person: their login, their name where the host gives one,
|
|
64
|
+
/** One person: their login, their name where the host gives one, what they are in the workspace, and that they are out when they are. */
|
|
61
65
|
export function formatPersonLine(person: RestPerson): string {
|
|
66
|
+
const standing = person.status === "active" ? person.role : `${person.role}, deactivated`;
|
|
62
67
|
return person.name === null
|
|
63
|
-
? `${person.login} (${
|
|
64
|
-
: `${person.login} ${person.name} (${
|
|
68
|
+
? `${person.login} (${standing})`
|
|
69
|
+
: `${person.login} ${person.name} (${standing})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The provider a listing names, as people call it. */
|
|
73
|
+
const HOST_NAMES = { gh: "GitHub", google: "Google" } as const;
|
|
74
|
+
|
|
75
|
+
/** One login let in that nobody has signed in with yet (ADR-0022). */
|
|
76
|
+
export function formatListingLine(listing: RestListing): string {
|
|
77
|
+
return `${listing.login} (let in at ${HOST_NAMES[listing.host]}, not signed in yet)`;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
/** One member of a project: their login, what they are in it, and since when. */
|
|
@@ -203,17 +216,27 @@ const SKILLS_WORDS: Readonly<Record<Exclude<RestSkills["status"], "none" | "read
|
|
|
203
216
|
unavailable: "SkillCDN could not be reached; ask again later.",
|
|
204
217
|
};
|
|
205
218
|
|
|
206
|
-
/**
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
219
|
+
/**
|
|
220
|
+
* The organization's skills: where they are, each with what an agent loads it by, where a
|
|
221
|
+
* person connects an agent that has no connection to the address, and where the console's own
|
|
222
|
+
* skills are on this machine, when the command knows.
|
|
223
|
+
*/
|
|
224
|
+
export function formatSkills(skills: RestSkills, ownSkills?: string): string {
|
|
225
|
+
const own =
|
|
226
|
+
ownSkills === undefined
|
|
227
|
+
? []
|
|
228
|
+
: ["", `The console's own skills, for working the board: ${ownSkills}`];
|
|
229
|
+
if (skills.status === "none" || skills.address === null || skills.page === null) {
|
|
230
|
+
return ["No skills address: neither this project nor the console names one.", ...own].join(
|
|
231
|
+
"\n",
|
|
232
|
+
);
|
|
210
233
|
}
|
|
211
234
|
const head = `skills at ${skills.address}, served by ${skills.source}: ${skills.status}`;
|
|
212
235
|
if (skills.status !== "ready") {
|
|
213
|
-
return
|
|
236
|
+
return [head, SKILLS_WORDS[skills.status], ...own].join("\n");
|
|
214
237
|
}
|
|
215
238
|
if (skills.items.length === 0) {
|
|
216
|
-
return
|
|
239
|
+
return [head, "No skills at this address.", ...own].join("\n");
|
|
217
240
|
}
|
|
218
241
|
const lines = [head];
|
|
219
242
|
for (const skill of skills.items) {
|
|
@@ -221,11 +244,41 @@ export function formatSkills(skills: RestSkills): string {
|
|
|
221
244
|
}
|
|
222
245
|
lines.push(
|
|
223
246
|
"",
|
|
224
|
-
|
|
247
|
+
`Load a skill by its URI through your SkillCDN connection to this address; an agent without one is connected by a person at ${skills.page}. --json adds the page of each, for a person.`,
|
|
248
|
+
...own,
|
|
225
249
|
);
|
|
226
250
|
return lines.join("\n");
|
|
227
251
|
}
|
|
228
252
|
|
|
253
|
+
/** One half of the rules: where it comes from, and the text as it stands. */
|
|
254
|
+
function formatRulesHalf(where: "the workspace" | "this project", rules: RestRules): string {
|
|
255
|
+
const page = rules.page;
|
|
256
|
+
const facts =
|
|
257
|
+
page === null
|
|
258
|
+
? undefined
|
|
259
|
+
: `v${page.version}, by ${writer(page.updatedBy, page.agent)}; ${page.updatedAt}`;
|
|
260
|
+
if (rules.source === "none" || rules.body === null) {
|
|
261
|
+
return `Rules of ${where}: none of its own; the workspace's hold.`;
|
|
262
|
+
}
|
|
263
|
+
const head =
|
|
264
|
+
rules.source === "reference"
|
|
265
|
+
? `Rules of ${where}: the console's own, until an administrator writes the organization's (console rules write --workspace).`
|
|
266
|
+
: `Rules of ${where} (${facts ?? "written"}):`;
|
|
267
|
+
return `${head}\n\n${rules.body.trimEnd()}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The rules that hold: the workspace's, and what the project adds when the command works in one. */
|
|
271
|
+
export function formatRules(rules: RestProjectRules | RestRules): string {
|
|
272
|
+
if ("workspace" in rules) {
|
|
273
|
+
return [
|
|
274
|
+
formatRulesHalf("the workspace", rules.workspace),
|
|
275
|
+
"",
|
|
276
|
+
formatRulesHalf("this project", rules.project),
|
|
277
|
+
].join("\n");
|
|
278
|
+
}
|
|
279
|
+
return formatRulesHalf("the workspace", rules);
|
|
280
|
+
}
|
|
281
|
+
|
|
229
282
|
/** How far a goal is, from its tasks: the done, the at work, and the rest, in one phrase. */
|
|
230
283
|
function progressOf(goal: RestGoal): string {
|
|
231
284
|
const counts = goal.taskCounts;
|
|
@@ -282,6 +335,17 @@ export function formatWork(work: RestWork): string {
|
|
|
282
335
|
return lines.join("\n");
|
|
283
336
|
}
|
|
284
337
|
|
|
338
|
+
/** Another person's plate: their open tasks, each with its project; no decisions, which wait for anyone. */
|
|
339
|
+
export function formatPersonWork(person: RestPerson, work: RestPersonWork): string {
|
|
340
|
+
if (work.tasks.length === 0) {
|
|
341
|
+
return `No open task is assigned to ${person.login}.`;
|
|
342
|
+
}
|
|
343
|
+
return [
|
|
344
|
+
`tasks assigned to ${person.login}:`,
|
|
345
|
+
...work.tasks.map(({ project, task }) => ` ${project.key} ${formatTaskLine(task)}`),
|
|
346
|
+
].join("\n");
|
|
347
|
+
}
|
|
348
|
+
|
|
285
349
|
export function formatDecisionLine(decision: RestDecision): string {
|
|
286
350
|
const state = decision.answer === null ? "waiting" : `answered ${decision.answer.option}`;
|
|
287
351
|
return `${decision.id} ${state} ${decision.question} (asked by ${asker(decision)})`;
|
package/src/cli/help.ts
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
MAX_PROJECT_SYMBOL_LENGTH,
|
|
21
21
|
MAX_QUESTION_LENGTH,
|
|
22
22
|
MAX_REPORTS_PER_RUN,
|
|
23
|
+
MAX_RULES_LENGTH,
|
|
23
24
|
MAX_SUMMARY_LENGTH,
|
|
24
25
|
MAX_TITLE_LENGTH,
|
|
25
26
|
MAX_VERSIONS_PER_DOCUMENT,
|
|
@@ -83,7 +84,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
83
84
|
login: {
|
|
84
85
|
usage: "login --url <origin> [--agent <name>] [--token-stdin]",
|
|
85
86
|
about:
|
|
86
|
-
"Connects this agent to the console: shows an address and a code, which a person opens signed in, names the agent and approves; the command then receives a token of its own and keeps it, with the console's address, in your home directory, never showing it. An agent running the command relays the address and the code to its person and waits. --agent says what the agent calls itself. --token-stdin reads a token made by hand from standard input instead, for a script or a job.",
|
|
87
|
+
"Connects this agent to the console: shows an address and a code, which a person opens signed in, names the agent and approves; the command then receives a token of its own and keeps it, with the console's address, in your home directory, never showing it. An agent running the command relays the address and the code to its person and waits. --agent says what the agent calls itself; left out, the command names the agent it runs under where its environment says so (Claude Code, Codex), else An agent; the machine's name follows, and the pages show the name before 'on' with the whole on hover. --token-stdin reads a token made by hand from standard input instead, for a script or a job.",
|
|
87
88
|
refusals: [
|
|
88
89
|
"connect.not_found: the code was not approved in time, or the person said it was not theirs; run login again",
|
|
89
90
|
"the console does not know the token given: make one on the Agents page and try again",
|
|
@@ -92,7 +93,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
92
93
|
logout: {
|
|
93
94
|
usage: "logout",
|
|
94
95
|
about:
|
|
95
|
-
"
|
|
96
|
+
"Signs this agent out: gives the token up at the console, so that it is good for nothing from now on, and forgets it with the address kept here. A console out of reach leaves the token to the Agents page.",
|
|
96
97
|
},
|
|
97
98
|
whoami: {
|
|
98
99
|
usage: "whoami",
|
|
@@ -111,9 +112,9 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
111
112
|
refusals: ["project.not_found: no project has the key, or it is not yours to see"],
|
|
112
113
|
},
|
|
113
114
|
work: {
|
|
114
|
-
usage: "work",
|
|
115
|
+
usage: "work [--person <login>]",
|
|
115
116
|
about:
|
|
116
|
-
"What is on your plate across the projects you may see: the open tasks assigned to you, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project.",
|
|
117
|
+
"What is on your plate across the projects you may see: the open tasks assigned to you, the sooner due first, each with its project; and the decisions waiting for a person. Needs no project. --person <login> shows another person's plate, for an administrator or for an owner of a project they are in: their open tasks within the projects you may see, without the decisions, which wait for anyone.",
|
|
117
118
|
},
|
|
118
119
|
tasks: {
|
|
119
120
|
usage:
|
|
@@ -159,15 +160,19 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
159
160
|
people: {
|
|
160
161
|
usage: "people",
|
|
161
162
|
about:
|
|
162
|
-
"Everyone of the workspace, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in. The logins are what --assignee, member and person
|
|
163
|
+
"Everyone of the workspace, by login, with their name where the host gives one and what they are in it: admin, who configures the workspace, or member, who works in the projects they are in; a person taken out is marked deactivated. Then the logins an administrator let in that nobody has signed in with yet. The logins are what --assignee, member and person take.",
|
|
163
164
|
},
|
|
164
165
|
person: {
|
|
165
|
-
usage:
|
|
166
|
+
usage:
|
|
167
|
+
"person role <login> <admin|member> | person add <login> [--host gh|google] | person remove <login> | person deactivate <login> | person reactivate <login>",
|
|
166
168
|
about:
|
|
167
|
-
"
|
|
169
|
+
"For an administrator: the person themselves on the People page, or their agent here, named on it. role says what a person is in the workspace; the board keeps at least one administrator in. add lets a login in: whoever signs in with that account at that host is a person from then on; the host is read off the login (an address is Google's, anything else GitHub's) unless --host says. remove takes a login away that nobody has signed in with yet. deactivate takes a person out: refused on every request at once, their agents with them, kept with everything they did, and listed nowhere; reactivate takes them back in. A person the organization let in comes and goes with it, and is neither.",
|
|
168
170
|
refusals: [
|
|
169
|
-
"auth.forbidden: only an administrator says what
|
|
170
|
-
"person.last_admin: the board keeps at least one administrator; make another one first",
|
|
171
|
+
"auth.forbidden: only an administrator says who is in and what they are",
|
|
172
|
+
"person.last_admin: the board keeps at least one administrator in; make another one first",
|
|
173
|
+
"person.of_organization: the organization says whether that person is in",
|
|
174
|
+
"listing.exists: the login is let in already, or is a person's who is in",
|
|
175
|
+
"listing.unknown_host: nobody signs in through that provider here",
|
|
171
176
|
],
|
|
172
177
|
},
|
|
173
178
|
project: {
|
|
@@ -228,7 +233,20 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
228
233
|
skills: {
|
|
229
234
|
usage: "skills",
|
|
230
235
|
about:
|
|
231
|
-
"The organization's skills, as SkillCDN serves them at the
|
|
236
|
+
"The organization's skills, as SkillCDN serves them at the project's address: each with its description and the URI you load it by through your own SkillCDN connection, which a person makes at the address's page when you have none; and where the console's own skills are on this machine. --json adds the page of each, for a person.",
|
|
237
|
+
},
|
|
238
|
+
rules: {
|
|
239
|
+
usage:
|
|
240
|
+
"rules | rules write --body <markdown>|--file <path>|-|none [--workspace] [--base <version>]",
|
|
241
|
+
about:
|
|
242
|
+
"What this organization asks of agents (ADR-0023): the workspace's rules, the console's own until an administrator writes the organization's, and this project's own when it has them. Read them before taking a task; take prints them again. rules write writes this project's rules, for an owner, or with --workspace the workspace's, for an administrator; none puts the written rules away, so that the console's own hold again, or the project adds none. Without a project named, rules shows the workspace's alone.",
|
|
243
|
+
limits: [
|
|
244
|
+
`body: Markdown, up to ${MAX_RULES_LENGTH} characters; the console's own rules, written as they are, write nothing`,
|
|
245
|
+
],
|
|
246
|
+
refusals: [
|
|
247
|
+
"auth.forbidden: an owner writes a project's rules, an administrator the workspace's",
|
|
248
|
+
"document.conflict: the rules have moved on since --base; read them again",
|
|
249
|
+
],
|
|
232
250
|
},
|
|
233
251
|
take: {
|
|
234
252
|
usage: "take <number|id> [--agent <name>]",
|
|
@@ -384,6 +402,7 @@ export const COMMAND_HELP: Readonly<Record<string, CommandHelp>> = {
|
|
|
384
402
|
const preamble = (version: string | undefined): string =>
|
|
385
403
|
`console${version === undefined ? "" : ` ${version}`}: the board of your organization's work with AI agents, from the command line.
|
|
386
404
|
You act as the person whose token you hold; everything you send is shown to people as text.
|
|
405
|
+
The rules of this organization come with the task: console take prints them; console rules reads them any time.
|
|
387
406
|
|
|
388
407
|
Usage: console <command> [arguments] [--json] [--url <origin>] [--project <key>]
|
|
389
408
|
`;
|
|
@@ -425,6 +444,7 @@ const SECTIONS: readonly { readonly title: string; readonly commands: readonly s
|
|
|
425
444
|
"runs",
|
|
426
445
|
"run",
|
|
427
446
|
"skills",
|
|
447
|
+
"rules",
|
|
428
448
|
],
|
|
429
449
|
},
|
|
430
450
|
{ title: "Working", commands: ["take", "report", "hand-in", "ask", "finish", "fail", "abandon"] },
|
package/src/cli/main.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { hostname } from "node:os";
|
|
4
5
|
import process from "node:process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import * as z from "zod/mini";
|
|
6
8
|
import { runCli } from "./cli.js";
|
|
7
9
|
import { createFileStore, defaultConfigDir } from "./credentials.js";
|
|
@@ -16,6 +18,8 @@ import { readBytesAt, readFileAt, readStdin } from "./terminal.js";
|
|
|
16
18
|
const manifest = z
|
|
17
19
|
.object({ version: z.string() })
|
|
18
20
|
.safeParse(createRequire(import.meta.url)("../../package.json"));
|
|
21
|
+
/** The console's own skills, shipped with the package beside dist/ (ADR-0015); not there in a checkout before packing. */
|
|
22
|
+
const skillsDir = fileURLToPath(new URL("../../skills/", import.meta.url));
|
|
19
23
|
|
|
20
24
|
process.exitCode = await runCli(process.argv.slice(2), {
|
|
21
25
|
env: process.env,
|
|
@@ -35,5 +39,6 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
35
39
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
36
40
|
now: () => Date.now(),
|
|
37
41
|
version: manifest.success ? manifest.data.version : undefined,
|
|
42
|
+
skillsDir: existsSync(skillsDir) ? skillsDir : undefined,
|
|
38
43
|
listen: listenOn,
|
|
39
44
|
});
|